选型 hyperf/jwt-auth hyperf/middleware spatie/laravel-permission 思路移植 Redis 权限缓存 --- 完整链路 请求 └─ JwtMiddleware# 解析 token → 注入租户/用户上下文└─ TenantMiddleware# 切换 DB Schema / 设置全局 Scope└─ PermMiddleware# 鉴权接口级└─ Controller └─ ReportService └─ RowPolicyScope# 行级过滤注入 SQL--- 一、JWT 中间件 — 身份识别?php // app/Middleware/JwtMiddleware.php namespace App\Middleware;use Hyperf\Context\Context;use Hyperf\HttpServer\Contract\RequestInterface;use Phper666\JWTAuth\JWT;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;use Psr\Http\Server\MiddlewareInterface;use Psr\Http\Server\RequestHandlerInterface;class JwtMiddleware implements MiddlewareInterface{publicfunction__construct(privatereadonlyJWT$jwt){}publicfunctionprocess(ServerRequestInterface$request, RequestHandlerInterface$handler): ResponseInterface{$payload$this-jwt-getParserData();// 协程安全自动取 Authorization header // 注入协程上下文全链路可读 Context::set(auth,[user_id(int)$payload[uid],tenant_id(int)$payload[tid],roles(array)$payload[roles],dept_id(int)$payload[dept_id],]);return$handler-handle($request);}}--- 二、租户中间件 — 数据隔离?php // app/Middleware/TenantMiddleware.php namespace App\Middleware;use Hyperf\Context\Context;use Hyperf\DbConnection\Db;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;use Psr\Http\Server\MiddlewareInterface;use Psr\Http\Server\RequestHandlerInterface;class TenantMiddleware implements MiddlewareInterface{publicfunctionprocess(ServerRequestInterface$request, RequestHandlerInterface$handler): ResponseInterface{$tenantIdContext::get(auth)[tenant_id];// 方案ASchema 隔离强隔离每租户独立库 Db::statement(SET search_path TO tenant_{$tenantId});// 方案B共享表 全局 Scope弱隔离本方案采用 Context::set(tenant_scope,[tenant_id$tenantId]);return$handler-handle($request);}}--- 三、权限中间件 — 接口级鉴权?php // app/Middleware/PermMiddleware.php namespace App\Middleware;use Hyperf\Context\Context;use Hyperf\HttpServer\Router\Dispatched;use App\Service\PermissionService;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface;use Psr\Http\Server\MiddlewareInterface;use Psr\Http\Server\RequestHandlerInterface;class PermMiddleware implements MiddlewareInterface{publicfunction__construct(privatereadonlyPermissionService$perm){}publicfunctionprocess(ServerRequestInterface$request, RequestHandlerInterface$handler): ResponseInterface{$authContext::get(auth);$dispatched$request-getAttribute(Dispatched::class);$required$dispatched-handler-options[permission]?? null;if($required!$this-perm-has($auth[user_id],$required)){returnresponse()-json([code403,msgForbidden],403);}return$handler-handle($request);}}// 路由注册时声明所需权限 Router::get(/report/export,[ReportController::class,export],[middleware[JwtMiddleware::class, TenantMiddleware::class, PermMiddleware::class],permissionreport:export,]);--- 四、权限服务 — Redis 缓存权限树?php // app/Service/PermissionService.php namespace App\Service;use Hyperf\Cache\Annotation\Cacheable;use Hyperf\Cache\Annotation\CacheEvict;use Hyperf\DbConnection\Db;class PermissionService{// 用户权限集合缓存10分钟#[Cacheable(prefix: perm, value: #{userId}, ttl: 600)]publicfunctionall(int$userId): array{returnDb::table(permissions as p)-join(role_permissions as rp,rp.permission_id,p.id)-join(user_roles as ur,ur.role_id,rp.role_id)-where(ur.user_id,$userId)-pluck(p.code)-all();}publicfunctionhas(int$userId, string$code): bool{returnin_array($code,$this-all($userId),true);}#[CacheEvict(prefix: perm, value: #{userId})]publicfunctionevict(int$userId): void{}}--- 五、行级策略 — 核心数据过滤?php // app/Policy/RowPolicy.php namespace App\Policy;use Hyperf\Context\Context;use Hyperf\DbConnection\Db;/** * 根据用户身份动态注入行级过滤条件 * 策略优先级超管租户管理员部门主管普通用户 */ class RowPolicy{publicfunctionapply(\Hyperf\Database\Query\Builder$query, string$table): void{$authContext::get(auth);$roles$auth[roles];match(true){in_array(super_admin,$roles)null, // 全量 in_array(tenant_admin,$roles)$query-where({$table}.tenant_id,$auth[tenant_id]), in_array(dept_manager,$roles)$query-whereIn({$table}.dept_id,$this-subDepts($auth[dept_id])), default$query-where({$table}.created_by,$auth[user_id]),};}// 递归获取部门树Redis 缓存 privatefunctionsubDepts(int$deptId): array{$keydept_tree:{$deptId};if($cachedredis()-get($key))returnunserialize($cached);$ids$this-fetchSubDepts($deptId);redis()-setex($key,3600, serialize($ids));return$ids;}privatefunctionfetchSubDepts(int$parentId): array{$childrenDb::table(departments)-where(parent_id,$parentId)-pluck(id)-all();foreach($childrenas$id){$childrenarray_merge($children,$this-fetchSubDepts($id));}returnarray_merge([$parentId],$children);}}--- 六、报表服务 — 组装完整查询?php // app/Service/ReportService.php namespace App\Service;use App\Policy\RowPolicy;use Hyperf\Context\Context;use Hyperf\DbConnection\Db;class ReportService{publicfunction__construct(privatereadonlyRowPolicy$policy){}publicfunctiongetData(int$templateId, array$filters):\Generator{$tenantScopeContext::get(tenant_scope);$lastId0;do{$queryDb::table(report_data as rd)-where(rd.template_id,$templateId)-where(rd.tenant_id,$tenantScope[tenant_id])// 租户隔离 -where(rd.id,,$lastId)-where($filters)-orderBy(rd.id)-limit(5000);$this-policy-apply($query,rd);// 注入行级权限过滤$rows$query-get();if($rows-isEmpty())break;$lastId$rows-last()-id;yield$rows-all();unset($rows);}while(true);}}七。controller?php9// app/Controller/ReportController.php namespace App\Controller;use App\Service\ReportService;use App\Service\TemplateService;use Hyperf\HttpServer\Annotation\Controller;use Hyperf\HttpServer\Annotation\Get;use Hyperf\HttpServer\Contract\RequestInterface;use Hyperf\HttpServer\Contract\ResponseInterface;use OpenSpout\Writer\XLSX\Writer;use OpenSpout\Common\Entity\Row;#[Controller(prefix: /report)]class ReportController{publicfunction__construct(privatereadonlyReportService$report, privatereadonlyTemplateService$template,){}#[Get(/export/{templateId}, options: [permission report:export])]publicfunctionexport(int$templateId, RequestInterface$request, ResponseInterface$response):\Psr\Http\Message\ResponseInterface{$tpl$this-template-get($templateId);$filters$request-query();$tmpFiletempnam(sys_get_temp_dir(),rpt_)..xlsx;$writernew Writer();$writer-openToFile($tmpFile);$writer-addRow(Row::fromValues($tpl[headers]));foreach($this-report-getData($templateId,$filters)as$batch){$writer-addRows(array_map(fn($r)Row::fromValues(array_values((array)$r)),$batch));}$writer-close();return$response-withHeader(Content-Type,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet)-withHeader(Content-Disposition,attachment; filename\report_{$templateId}.xlsx\)-withBody(new\Hyperf\HttpMessage\Stream\SwooleFileStream($tmpFile));}}--- 八、完整链路数据流 POST /report/export/42?deptsales │ ├─ JwtMiddleware │ └─ 解析 Bearer token │ Context::set(auth,{uid:9, tid:3, roles:[dept_manager], dept_id:7})│ ├─ TenantMiddleware │ └─ Context::set(tenant_scope,{tenant_id:3})│ ├─ PermMiddleware │ └─ PermissionService::has(9,report:export)│ └─ Redis HIT →[report:export,report:view]✓ │ └─ ReportController::export(42)├─ TemplateService::get(42)→ Redis L3 HIT ├─ ReportService::getData(42,...)→ 游标分页 │ └─ RowPolicy::apply(query,rd)│ └─roles[dept_manager]│ └─ WHERE rd.dept_id IN(7,12,15)← 部门树展开 │ AND rd.tenant_id3← 租户隔离 │ AND rd.template_id42│ AND rd.id:last_id ← 游标分页 └─ OpenSpout 流式写 XLSX → 响应流 --- 九、权限矩阵 ┌──────────────┬──────────────────────────────────────────┬──────────────┐ │ 角色 │ 行级过滤条件 │ 可见数据范围 │ ├──────────────┼──────────────────────────────────────────┼──────────────┤ │ super_admin │ 无 │ 全平台 │ │ tenant_admin │ tenant_id:tid │ 本租户全量 │ │ dept_manager │ dept_id IN(本部门所有子部门)│ 部门树 │ │ user │ created_by:uid │ 仅本人 │ └──────────────┴──────────────────────────────────────────┴──────────────┘ --- 十、安全加固要点 //1. 防越权导出前校验模板归属当前租户$tplDb::table(report_templates)-where(id,$templateId)-where(tenant_id, Context::get(tenant_scope)[tenant_id])// 必须 -firstOrFail();//2. 防注入filters 白名单校验禁止原始 SQL 片段$allowed[status,dept_id,created_at];$filtersarray_intersect_key($request-query(), array_flip($allowed));//3. 审计日志每次导出记录操作人、条件、行数 Db::table(audit_logs)-insert([user_idContext::get(auth)[user_id],actionreport_export,template_id$templateId,filtersjson_encode($filters),ip$request-getServerParams()[remote_addr],created_attime(),]);--- 十一、关键设计决策 ┌──────────────┬────────────────────────┬─────────────────────────────────────────────┐ │ 问题 │ 决策 │ 原因 │ ├──────────────┼────────────────────────┼─────────────────────────────────────────────┤ │ 权限存哪里 │ Redis 缓存 10min │ 避免每请求查 DB高并发下 DB 压力骤降 │ ├──────────────┼────────────────────────┼─────────────────────────────────────────────┤ │ 行级过滤时机 │ Service 层注入 SQL │ 比应用层过滤节省90% 内存和传输 │ ├──────────────┼────────────────────────┼─────────────────────────────────────────────┤ │ 租户隔离方式 │ 共享表 tenant_id │ Schema 隔离运维成本高中小租户数共享表足够 │ ├──────────────┼────────────────────────┼─────────────────────────────────────────────┤ │ 部门树缓存 │ Redis 1h │ 组织架构变更低频缓存命中率99% │ ├──────────────┼────────────────────────┼─────────────────────────────────────────────┤ │ 越权防护 │ 查询时强制带 tenant_id │ 即使 JWT 被伪造也无法跨租户读数据 │ └──────────────┴────────────────────────┴─────────────────────────────────────────────┘