权限校验中间件必须按认证→权限顺序注册,依赖路由命名而非控制器/动作拼接,权限数据预加载至session并支持主动刷新,超管逻辑需短路,can()函数须支持通配符与多条件内存比对。

权限校验中间件写不对,逻辑漏洞就藏在“看似放行”的地方——不是没校验,而是校验依据错了、缓存没清、超管短路没做全。
中间件注册顺序必须是认证 → 权限
权限中间件依赖用户对象,但 $request->user() 或 $request->getAttribute('user') 在认证中间件执行前为空。ThinkPHP、Laravel 等框架里,CheckAuth 必须排在 CheckPermission 前面,否则 isSuperAdmin() 判断直接失效。
- 错误做法:在
__construct()里调用Auth::user()—— 此时 session 还未绑定,返回 null - 正确时机:只在
handle()方法内通过$request->getAttribute('user')取值(前提是前置认证已注入) - 注册位置:ThinkPHP 的
app/middleware.php中,'http'数组末尾;Laravel 的app/Http/Kernel.php中,确保auth中间件在自定义权限中间件之前
路由名才是唯一可信的校验依据
别用 $request->controller() . '/' . $request->action() 拼路径——驼峰转换、多应用前缀、大小写策略会让它和数据库里的 auth_rule.name 对不上。真正稳定的标识是 $request->rule()->getName(),前提是路由显式命名(如 Route::get('/users', ...)->name('user.list');)。
- 常见错配现象:
Call to a member function getName() on null→ 先判空:if (!$request->route()) { return $next($request); } - 数据库里
auth_rule.name必须存路由名(如admin.user.delete),不是 URL(/admin/users/1)或方法名(deleteUser) - 多应用项目必须带前缀:
api.v1.order.create,否则不同模块权限会串
权限数据必须预加载进 session,且支持主动刷新
每次请求都查库 → QPS 上去 DB 直接拖垮。但只靠 Redis 缓存又怕角色变更后权限不生效。折中方案是:登录时查一次并存入 $_SESSION['permissions'],变更时主动清 session 或更新该数组。
立即学习“PHP免费学习笔记(深入)”;
- 存储格式必须是字符串数组:
['post.create', 'user.delete', 'setting.*'],不用 ID,避免调试困难 - SQL 必须加
DISTINCT:角色继承时容易重复拉取同一权限,导致in_array()失效 - 超管逻辑要短路:在中间件开头就判断
isSuperAdmin(),return true,别让它进后续权限比对流程 - 游客处理要统一:未登录直接跳转
/login,别留空 session 或返回 403——前端体验断裂
can() 函数必须支持通配符和多条件表达式
裸写 in_array('post.edit', $_SESSION['permissions']) 无法应对真实业务:比如「编辑自己文章 or 有全局编辑权」、「所有 post.* 操作」。封装成 can() 是必须的,且只做内存比对,不查库。
- 支持单字符串:
can('post.edit') - 支持通配符:
can('post.*')→ 匹配post.create、post.delete - 支持数组(OR 逻辑):
can(['user.delete', 'user.ban'])→ 满足任一即通过 - 返回布尔值,不抛异常——由控制器决定是返回 JSON 403 还是重定向,避免中间件越权响应
- 禁止在
can()内部调用 DB 或 Cache,它只是个快速过滤器
最难绷的其实是缓存一致性:用户改了角色,你清了 Redis,但忘了清 PHP session 里的 $_SESSION['permissions'],结果旧权限还在内存里跑着。这种漏洞不会报错,只会让安全策略形同虚设。



















