权限节点必须用小写点号格式rule_name(如admin.user.edit)作唯一标识,因ThinkPHP中间件依赖request()->rule()->getName()获取路由名,需与auth_rule表中rule_name严格一致才能匹配校验。

权限节点必须用 rule_name(如 admin.user.edit)作唯一标识,不能用中文、ID 或 URL 路径直接拼接——否则中间件根本拦不住接口请求。
为什么 admin/user/edit 和 admin.user.edit 必须严格对应
ThinkPHP 中间件靠 request()->rule()->getName() 拿到的是路由规则名(如 admin/user/edit),而权限节点表 auth_rule 里存的必须是映射后的小写点号格式(admin.user.edit)。两者不一致,in_array 或缓存查就永远失败。
- 路由定义是
Route::post('admin/user/delete', [...])→ 规则名就是admin/user/delete - 对应权限节点
rule_name必须填admin.user.delete,不是user_delete、不是删除用户、也不能漏掉admin.前缀 - 前端按钮权限判断也得用同一套命名,比如 Layui 的
data-auth="admin.user.delete" - 别在中间件里手动把斜杠替换成点号——TP6 的
Str::snake()不可靠,建议写死映射逻辑:str_replace('/', '.', $ruleName)
auth_rule 表字段设计最容易翻车的三个地方
很多人建完表跑两小时才发现菜单能显示、接口全放行,问题就出在 auth_rule 的字段定义上。
-
rule_name字段必须设为UNIQUE索引,且类型为VARCHAR(100)——太短会截断长节点(如admin.order.export.detail) -
pid必须是INT类型且允许为NULL或默认值0;设成VARCHAR或默认''会导致递归菜单树无限循环或白屏 -
type字段建议只用menu和action两种值,别加button、api等自定义类型——中间件校验时通常只按这两类分流,多出来的类型会被忽略
中间件里查权限,为什么不能直接查数据库
每次请求都执行 JOIN 查询 auth_role_rule + auth_rule,QPS 上 50 就开始慢,而且缓存失效后容易打挂 DB。
立即学习“PHP免费学习笔记(深入)”;
- 正确做法:先查 Redis 缓存
auth_rule_map_{$roleId}(Set 结构,存所有该角色拥有的rule_name) - 缓存未命中才查 DB,查完立刻写入缓存,过期时间设为
1800(30 分钟)——太长权限变更延迟高,太短击穿风险大 - 关键点:缓存 key 必须带
$roleId,绝不能只缓存全量节点列表;否则 A 角色删了权限,B 角色还在用旧缓存,就出现误放行 - 权限变更后(比如分配新节点),必须主动执行
cache('auth_rule_map_'.$roleId, null)清空对应缓存,不能等自动过期
超级管理员为什么还是被拦截
数据库里 is_super = 1,但 admin/user/list 页面打开就 403——因为权限中间件压根没给超级管理员开绿灯。
- TP6 默认不识别
is_super字段,也不会跳过后续校验 - 必须在中间件
handle()最开头加判断:if ($user['is_super']) { return $next($request); } - 别把这个逻辑写在模型里或钩子里,要放在权限决策链最前端,否则可能被缓存、角色合并等逻辑覆盖
- 如果用了自定义
Auth类,记得重写check()方法,在里面提前返回true,而不是依赖默认实现
真正难的不是建三张表,而是让每个环节都对齐同一个 rule_name 字符串——路由、节点表、中间件、前端指令、缓存 key,差一个点号或大小写,整个权限链就断了。



















