CheckCompany.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. <?php
  2. namespace App\Http\Middleware;
  3. use App\Consts\ErrorConst;
  4. use App\Libs\Utils;
  5. use App\Facade\Site;
  6. use Closure;
  7. use App\Exceptions\ApiException;
  8. use Illuminate\Support\Facades\DB;
  9. class CheckCompany
  10. {
  11. /**
  12. * 请求参数资源归属校验(公司维度)
  13. *
  14. * 规则:
  15. * - 参数带资源 ID 时,校验该资源是否属于当前登录用户所在公司(cpid);
  16. * - 同一公司内不同用户默认视为共享,不校验 user_id;
  17. * - superadmin 等平台角色跳过校验(需要跨公司操作);
  18. * - 参数未传时跳过,必填校验由业务层负责。
  19. *
  20. * 校验项由 config/param_ownership.php 配置,支持参数名映射、
  21. * 批量参数(数组/逗号分隔/JSON)以及按路由覆盖。
  22. *
  23. * @param $request
  24. * @param Closure $next
  25. * @return mixed
  26. * @throws ApiException
  27. */
  28. public function handle($request, Closure $next)
  29. {
  30. $params = $request->all();
  31. $token = $request->header('d-token', '');
  32. if (!$token) {
  33. $token = getProp($params, 'd_token');
  34. if (!$token) Utils::throwError(ErrorConst::NOT_LOGIN);
  35. }
  36. $config = config('param_ownership', []);
  37. if (empty($config['enabled'])) {
  38. return $next($request);
  39. }
  40. // 平台角色需要跨公司操作,跳过归属校验
  41. if (in_array(Site::getRole(), getProp($config, 'skip_roles', []), true)) {
  42. return $next($request);
  43. }
  44. $cpid = Site::getCpid();
  45. if (!$cpid) {
  46. Utils::throwError(ErrorConst::NOT_ACCESS);
  47. }
  48. // 内部公司白名单:灰度期间先放过自己人,避免误拦
  49. if (in_array($cpid, getProp($config, 'skip_cpids', []), true)) {
  50. return $next($request);
  51. }
  52. $ownerColumn = getProp($config, 'owner_column', 'cpid');
  53. $path = $request->path();
  54. $routeMap = getProp($config, 'routes', []);
  55. // 汇总全部校验项:普通 / 批量 / 链式 / 链式批量 / 按路由覆盖
  56. $paramMap = getProp($config, 'params', []);
  57. foreach (getProp($config, 'array_params', []) as $key => $define) {
  58. $define['batch'] = true;
  59. $paramMap[$key] = $define;
  60. }
  61. foreach (getProp($config, 'linked_params', []) as $key => $define) {
  62. $paramMap[$key] = $define;
  63. }
  64. foreach (getProp($config, 'linked_array_params', []) as $key => $define) {
  65. $define['batch'] = true;
  66. $paramMap[$key] = $define;
  67. }
  68. if (isset($routeMap[$path]) && is_array($routeMap[$path])) {
  69. foreach ($routeMap[$path] as $key => $define) {
  70. $paramMap[$key] = $define;
  71. }
  72. }
  73. foreach ($paramMap as $key => $define) {
  74. $this->checkOwnership($params, $key, $define, $cpid, $ownerColumn);
  75. }
  76. return $next($request);
  77. }
  78. /**
  79. * 校验参数所指向资源的归属公司
  80. *
  81. * 支持两种模式:
  82. * - 直接校验:目标表自带归属列(cpid);
  83. * - 链式校验:目标表无归属列,先取记录再用外键到父表校验。
  84. *
  85. * @param array $params 请求参数
  86. * @param string $key 参数名
  87. * @param array $define 配置(table / key / type / batch / via)
  88. * @param int $cpid 当前公司ID
  89. * @param string $ownerColumn 归属列名
  90. * @throws ApiException
  91. */
  92. private function checkOwnership(array $params, string $key, array $define, int $cpid, string $ownerColumn): void
  93. {
  94. $table = getProp($define, 'table');
  95. if (!$table || !array_key_exists($key, $params)) {
  96. return;
  97. }
  98. $keyColumn = getProp($define, 'key', 'id');
  99. $type = getProp($define, 'type', 'int');
  100. $batch = (bool)getProp($define, 'batch', false);
  101. $via = getProp($define, 'via', null);
  102. foreach ($this->toIdList($params[$key], $batch, $type) as $id) {
  103. $owned = $via
  104. ? $this->ownedVia($table, $keyColumn, $id, $via, $cpid, $ownerColumn)
  105. : DB::table($table)->where($keyColumn, $id)->where($ownerColumn, $cpid)->exists();
  106. if (!$owned) {
  107. dLog('checkCompany')->info('资源归属校验未通过', [
  108. 'path' => request()->path(),
  109. 'param' => $key,
  110. 'id' => $id,
  111. 'table' => $table,
  112. 'cpid' => $cpid,
  113. 'uid' => Site::getUid(),
  114. ]);
  115. Utils::throwError(ErrorConst::NOT_ACCESS);
  116. }
  117. }
  118. }
  119. /**
  120. * 链式归属校验:按主键取记录,再用记录中的外键去父表校验归属
  121. *
  122. * @param string $table 目标表
  123. * @param string $keyColumn 目标表主键列
  124. * @param mixed $id 主键值
  125. * @param array $via ['local' => 外键列, 'table' => 父表, 'key' => 父表主键列]
  126. * @param int $cpid 当前公司ID
  127. * @param string $ownerColumn 归属列名
  128. * @return bool
  129. */
  130. private function ownedVia(string $table, string $keyColumn, $id, array $via, int $cpid, string $ownerColumn): bool
  131. {
  132. $local = getProp($via, 'local');
  133. $refTable = getProp($via, 'table');
  134. $refKey = getProp($via, 'key', 'id');
  135. if (!$local || !$refTable) {
  136. return false;
  137. }
  138. // 资源不存在时按无权限处理(fail-closed)
  139. $row = DB::table($table)->where($keyColumn, $id)->first([$local]);
  140. if (!$row) {
  141. return false;
  142. }
  143. $refValue = getProp($row, $local);
  144. if ($refValue === null || $refValue === '') {
  145. return false;
  146. }
  147. return DB::table($refTable)->where($refKey, $refValue)->where($ownerColumn, $cpid)->exists();
  148. }
  149. /**
  150. * 统一转换为 ID 列表(兼容单值、数组、逗号分隔、JSON 数组)
  151. *
  152. * @param mixed $value
  153. * @param bool $batch 是否允许逗号分隔 / JSON 数组
  154. * @param string $type 主键类型:int(默认)/ string
  155. * @return array
  156. */
  157. private function toIdList($value, bool $batch, string $type = 'int'): array
  158. {
  159. if (is_array($value)) {
  160. $items = $value;
  161. } elseif ($batch && is_string($value)) {
  162. $decoded = json_decode($value, true);
  163. $items = is_array($decoded) ? $decoded : explode(',', $value);
  164. } else {
  165. $items = [$value];
  166. }
  167. $ids = [];
  168. foreach ($items as $item) {
  169. if (is_array($item)) {
  170. $ids = array_merge($ids, $this->toIdList($item, $batch, $type));
  171. continue;
  172. }
  173. if ($type === 'string') {
  174. // 字符串主键(如 mp_episode_segments.segment_id)需保留原值
  175. $val = trim((string)$item);
  176. if ($val !== '') {
  177. $ids[] = $val;
  178. }
  179. continue;
  180. }
  181. $id = (int)$item;
  182. if ($id > 0) {
  183. $ids[] = $id;
  184. }
  185. }
  186. return array_values(array_unique($ids));
  187. }
  188. }