GateRequest.php 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. <?php
  2. namespace App\Http\Requests;
  3. use App\Services\MenuService;
  4. use Illuminate\Foundation\Http\FormRequest;
  5. use Illuminate\Support\Facades\Auth;
  6. use Illuminate\Support\Facades\Log;
  7. class GateRequest extends FormRequest
  8. {
  9. /**
  10. * @var string
  11. */
  12. protected $currentRoute;
  13. /**
  14. * @var array
  15. */
  16. protected $tracks;
  17. /**
  18. * 通用鉴权
  19. *
  20. * @return bool
  21. */
  22. public function authorize():bool
  23. {
  24. if (!$this->ajax() && !$this->authorizeGate())$this->sameOriginMatching();
  25. return true;
  26. }
  27. public function rules():array
  28. {
  29. return [];
  30. }
  31. /**
  32. * 授权门鉴定
  33. *
  34. * @return bool
  35. */
  36. public function authorizeGate():bool
  37. {
  38. $this->currentRoute = ltrim($this->getPathInfo(),"/");
  39. /** @var MenuService $service */
  40. $service = app("MenuService");
  41. if (!$this->matchingRoute($service->getVisibleMenuListAll()))return false;//全量匹配失败直接授权失败 此时正确做法应该是404
  42. $correctStack = $this->tracks; //全量匹配下的 正确栈节点
  43. $this->tracks = [];//栈节点制空 存储目标节点
  44. if (!$this->matchingRoute($service->getVisibleFunctionList()))return false;//部分匹配下的授权失败
  45. return count($correctStack)==count($this->tracks); //正确栈与目标栈 对比
  46. }
  47. /**
  48. * 递归匹配路由
  49. *
  50. * 每次递归 各自记录自己的栈路径
  51. * 每次匹配成功都将当前栈路径替换全局栈路径
  52. * 替换后如果存在节点继续匹配 直至节点为空得出最终的栈终点
  53. */
  54. private function matchingRoute(array $routes,array $tagStack = [],bool $successMark = false):bool
  55. {
  56. foreach ($routes as $route){
  57. $currentStack = $tagStack;$currentStack[] = $route["name"];
  58. $match = $this->matching($route["route"] ?? "");
  59. if ($match){$this->tracks = $currentStack;$successMark = true;}
  60. if ($route["child"] ?? false){
  61. if ($this->matchingRoute($route["child"],$currentStack,$successMark))return true;
  62. }
  63. }
  64. return $successMark;
  65. }
  66. private function matching($tag):bool
  67. {
  68. if (!$tag)return false;
  69. $tag = explode("?",$tag)[0];//去除参数
  70. if ($this->currentRoute == $tag)return true; //相等
  71. if (strpos($tag,"*")===false)return false;//无泛匹配符
  72. $cr = explode("/",$this->currentRoute);
  73. $tr = explode("/",$tag);
  74. $crLen = count($cr);
  75. $trLen = count($tr);
  76. if ($crLen<$trLen)return false;
  77. if ($crLen>$trLen && $tr[$trLen-1]=='*'){
  78. $cr = array_slice($cr,0,$trLen);
  79. $crLen = $trLen;
  80. }
  81. if ($crLen!=$trLen)return false;
  82. foreach ($tr as $index=>$item){
  83. if ($item=='*')$tr[$index] = $cr[$index];
  84. }
  85. return implode("/",$cr) == implode("/",$tr);
  86. }
  87. /**
  88. * 递归一个同源节点 返回可用URL 深度取决于$this->tracks的长度
  89. *
  90. * @param array $routes
  91. * @param int $depth
  92. *
  93. * @return string|null
  94. */
  95. private function searchOrigin(array $routes,int $depth = 0):?string
  96. {
  97. $url = null;//预设返回URL
  98. foreach ($routes as $route){
  99. //如果目标栈长度大于当前深度 且 当前节点与目标节点name相同 且 当前节点有子节点存在,递归进入子节点,深度加1
  100. if (count($this->tracks)>$depth &&
  101. $this->tracks[$depth]==$route["name"] &&
  102. ($route["child"] ?? false))
  103. {
  104. $currentChildUrl = $this->searchOrigin($route["child"],$depth+1);
  105. if ($currentChildUrl)return $currentChildUrl;
  106. }
  107. if (!$url && ($route["route"] ?? ""))$url = $route["route"]; //取最近的同源的URL
  108. }
  109. return $url;
  110. }
  111. /**
  112. * 同源匹配 此时已经拿到同源节点 $this->tracks 需要在此节点上寻找一个允许的权限
  113. */
  114. public function sameOriginMatching()
  115. {
  116. $url = null;
  117. //目标节点存在
  118. if ($this->tracks){
  119. $url = $this->searchOrigin(app("MenuService")->getVisibleFunctionList());
  120. if ($url)Log::notice("同源策略跳转",["user"=>Auth::id(),"url"=>$url,"currentRoute"=>$this->currentRoute]);
  121. }
  122. if (!$url)$url = 'denied';
  123. header('Location: /'.$url,true,302);
  124. die();
  125. //目标节点为空的话说明一级节点都无权访问
  126. // 此时返回一级同源节点就无意义 会产生误导性,所以前往无权页面
  127. //正常情况下这个预设不会被正常用户触发
  128. }
  129. }