ThinkPHP动态权限分配需满足五点:auth_rule表必须含status字段并参与校验;多角色权限须自定义PermissionService::check()合并查询;菜单与操作权限需用type字段分离;权限缓存键须含角色更新时间戳;校验必须基于当前user_id,不可信前端传参。

ThinkPHP 用户权限分配不是配个中间件、调个 Auth::check() 就完事的。真正在生产环境跑起来,核心矛盾是:权限数据必须能运行时增删改,且每次请求都得准确反映最新状态——否则管理员刚分配完「财务导出」权限,用户刷新页面还是 403。
auth_rule 表必须带 status 字段并参与校验
很多项目把权限规则写进数据库就以为“动态”了,结果禁用某条规则后仍生效。根本原因是查权限时没过滤 status。
-
auth_rule表里每条记录必须有status字段(tinyint,默认 1 启用、0 禁用) - 所有权限查询 SQL 都要显式加
WHERE status = 1,不能只靠删除记录来“下线”权限 - 别把
status当成可选字段——它让你不用删数据就能灰度开关权限,也避免误删导致权限丢失 - 示例:查某角色启用的规则 ID 列表,必须写成
Db::name('role_access')->where(['role_id' => $id, 'status' => 1])->column('rule_id'),漏掉status条件就是埋雷
多角色权限不能依赖 Auth::check() 原生调用
ThinkPHP 6 官方 Auth::check() 默认只查用户第一个角色的权限,user_role 表里存了 3 个角色也没用。
- 必须自己实现合并逻辑:先查用户所有
role_id,再批量拉取这些角色关联的rule_id,最后去auth_rule表里匹配name - 别在中间件里直接调
Auth::check('admin/user/edit'),它内部走的是单角色查询路径 - 自定义服务类如
PermissionService::check($user, 'admin/user/edit')才可靠,且必须实时执行,不能缓存在 Request 或 Session 里 - 如果用了 think-auth 扩展,确认它的
getRoleList()返回的是数组,且你没在代码里手动取[0]
菜单和操作权限必须用 type 字段分离
把菜单路由(如 admin/dashboard)和按钮操作(如 admin/user/delete)混在同一个 auth_rule 表里,会导致“能看见菜单但点不开”或“能进页面但按钮不显示”。
立即学习“PHP免费学习笔记(深入)”;
- 给
auth_rule加type字段,值为'menu'或'action' - 菜单渲染前查
type = 'menu'且status = 1的记录;接口校验时只认type = 'action' - 前端拿到的菜单数据,必须是后端已按角色过滤后的结果,不能传全量菜单再靠 JS
if (hasPermission())隐藏——这等于把权限逻辑暴露给浏览器 -
path字段对菜单树很关键:存类似"0-1-5-12"的祖先路径,查子菜单时用LIKE '0-1-%',比递归查询快得多也更稳定
权限缓存键必须包含角色更新时间戳
用户权限变了,但页面还是旧的,八成是缓存没失效。ThinkPHP 默认缓存不感知角色权限变更,清缓存又太粗暴。
- 缓存 key 不能只用
user_id,得带上角色最后更新时间,比如'perm_' . $user_id . '_' . $roleUpdatedTime - 查
role表或role_access表的updated_at字段作为时间戳来源,角色权限一改,对应缓存自动失效 - 别用
Cache::clear()清全局缓存——会影响其他用户和其他业务模块 - 如果用 Redis,建议用哈希结构存权限列表,key 是
perm:{user_id},field 是rule_name,value 是布尔值,更新时只删对应 hash 即可
最易被忽略的一点:权限校验永远以当前登录用户的 user_id 为准,而不是前端传来的 role_id 或 permission_code。任何绕过用户上下文的判断,都是安全漏洞的温床。



















