| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- <?php
- namespace App\Http\Middleware;
- use App\Consts\ErrorConst;
- use App\Libs\Utils;
- use App\Facade\Site;
- use Closure;
- use App\Exceptions\ApiException;
- use Illuminate\Support\Facades\DB;
- class CheckCompany
- {
- /**
- * 请求参数资源归属校验(公司维度)
- *
- * 规则:
- * - 参数带资源 ID 时,校验该资源是否属于当前登录用户所在公司(cpid);
- * - 同一公司内不同用户默认视为共享,不校验 user_id;
- * - superadmin 等平台角色跳过校验(需要跨公司操作);
- * - 参数未传时跳过,必填校验由业务层负责。
- *
- * 校验项由 config/param_ownership.php 配置,支持参数名映射、
- * 批量参数(数组/逗号分隔/JSON)以及按路由覆盖。
- *
- * @param $request
- * @param Closure $next
- * @return mixed
- * @throws ApiException
- */
- public function handle($request, Closure $next)
- {
- $params = $request->all();
- $token = $request->header('d-token', '');
- if (!$token) {
- $token = getProp($params, 'd_token');
- if (!$token) Utils::throwError(ErrorConst::NOT_LOGIN);
- }
- $config = config('param_ownership', []);
- if (empty($config['enabled'])) {
- return $next($request);
- }
- // 平台角色需要跨公司操作,跳过归属校验
- if (in_array(Site::getRole(), getProp($config, 'skip_roles', []), true)) {
- return $next($request);
- }
- $cpid = Site::getCpid();
- if (!$cpid) {
- Utils::throwError(ErrorConst::NOT_ACCESS);
- }
- // 内部公司白名单:灰度期间先放过自己人,避免误拦
- if (in_array($cpid, getProp($config, 'skip_cpids', []), true)) {
- return $next($request);
- }
- $ownerColumn = getProp($config, 'owner_column', 'cpid');
- $path = $request->path();
- $routeMap = getProp($config, 'routes', []);
- // 汇总全部校验项:普通 / 批量 / 链式 / 链式批量 / 按路由覆盖
- $paramMap = getProp($config, 'params', []);
- foreach (getProp($config, 'array_params', []) as $key => $define) {
- $define['batch'] = true;
- $paramMap[$key] = $define;
- }
- foreach (getProp($config, 'linked_params', []) as $key => $define) {
- $paramMap[$key] = $define;
- }
- foreach (getProp($config, 'linked_array_params', []) as $key => $define) {
- $define['batch'] = true;
- $paramMap[$key] = $define;
- }
- if (isset($routeMap[$path]) && is_array($routeMap[$path])) {
- foreach ($routeMap[$path] as $key => $define) {
- $paramMap[$key] = $define;
- }
- }
- foreach ($paramMap as $key => $define) {
- $this->checkOwnership($params, $key, $define, $cpid, $ownerColumn);
- }
- return $next($request);
- }
- /**
- * 校验参数所指向资源的归属公司
- *
- * 支持两种模式:
- * - 直接校验:目标表自带归属列(cpid);
- * - 链式校验:目标表无归属列,先取记录再用外键到父表校验。
- *
- * @param array $params 请求参数
- * @param string $key 参数名
- * @param array $define 配置(table / key / type / batch / via)
- * @param int $cpid 当前公司ID
- * @param string $ownerColumn 归属列名
- * @throws ApiException
- */
- private function checkOwnership(array $params, string $key, array $define, int $cpid, string $ownerColumn): void
- {
- $table = getProp($define, 'table');
- if (!$table || !array_key_exists($key, $params)) {
- return;
- }
- $keyColumn = getProp($define, 'key', 'id');
- $type = getProp($define, 'type', 'int');
- $batch = (bool)getProp($define, 'batch', false);
- $via = getProp($define, 'via', null);
- foreach ($this->toIdList($params[$key], $batch, $type) as $id) {
- $owned = $via
- ? $this->ownedVia($table, $keyColumn, $id, $via, $cpid, $ownerColumn)
- : DB::table($table)->where($keyColumn, $id)->where($ownerColumn, $cpid)->exists();
- if (!$owned) {
- dLog('checkCompany')->info('资源归属校验未通过', [
- 'path' => request()->path(),
- 'param' => $key,
- 'id' => $id,
- 'table' => $table,
- 'cpid' => $cpid,
- 'uid' => Site::getUid(),
- ]);
- Utils::throwError(ErrorConst::NOT_ACCESS);
- }
- }
- }
- /**
- * 链式归属校验:按主键取记录,再用记录中的外键去父表校验归属
- *
- * @param string $table 目标表
- * @param string $keyColumn 目标表主键列
- * @param mixed $id 主键值
- * @param array $via ['local' => 外键列, 'table' => 父表, 'key' => 父表主键列]
- * @param int $cpid 当前公司ID
- * @param string $ownerColumn 归属列名
- * @return bool
- */
- private function ownedVia(string $table, string $keyColumn, $id, array $via, int $cpid, string $ownerColumn): bool
- {
- $local = getProp($via, 'local');
- $refTable = getProp($via, 'table');
- $refKey = getProp($via, 'key', 'id');
- if (!$local || !$refTable) {
- return false;
- }
- // 资源不存在时按无权限处理(fail-closed)
- $row = DB::table($table)->where($keyColumn, $id)->first([$local]);
- if (!$row) {
- return false;
- }
- $refValue = getProp($row, $local);
- if ($refValue === null || $refValue === '') {
- return false;
- }
- return DB::table($refTable)->where($refKey, $refValue)->where($ownerColumn, $cpid)->exists();
- }
- /**
- * 统一转换为 ID 列表(兼容单值、数组、逗号分隔、JSON 数组)
- *
- * @param mixed $value
- * @param bool $batch 是否允许逗号分隔 / JSON 数组
- * @param string $type 主键类型:int(默认)/ string
- * @return array
- */
- private function toIdList($value, bool $batch, string $type = 'int'): array
- {
- if (is_array($value)) {
- $items = $value;
- } elseif ($batch && is_string($value)) {
- $decoded = json_decode($value, true);
- $items = is_array($decoded) ? $decoded : explode(',', $value);
- } else {
- $items = [$value];
- }
- $ids = [];
- foreach ($items as $item) {
- if (is_array($item)) {
- $ids = array_merge($ids, $this->toIdList($item, $batch, $type));
- continue;
- }
- if ($type === 'string') {
- // 字符串主键(如 mp_episode_segments.segment_id)需保留原值
- $val = trim((string)$item);
- if ($val !== '') {
- $ids[] = $val;
- }
- continue;
- }
- $id = (int)$item;
- if ($id > 0) {
- $ids[] = $id;
- }
- }
- return array_values(array_unique($ids));
- }
- }
|