菜单权限需用path字段存储完整层级路径以支持高效查询,authrule表须用type区分菜单与操作,修改菜单或权限后必须手动刷新缓存,后端应只返回已授权菜单而非前端过滤。

菜单数据必须带完整层级路径才能做权限判断
ThinkPHP 本身不内置菜单权限联动逻辑,所谓“联动”本质是前端渲染 + 后端校验的配合。关键前提是菜单数据在数据库里不能只存 pid 和 name,必须能快速推导出某菜单是否属于当前用户可访问的「子树根节点」之下。最稳妥的做法是在保存/更新菜单时,同步维护一个 path 字段(如 "0-1-5-12"),用连字符分隔祖先 ID,包含自身。
这样查权限时不用递归查询,一条 SQL 就能拿到所有可见菜单:
SELECT * FROM menu WHERE path LIKE '0-1-%' AND id IN (SELECT menu_id FROM auth_rule WHERE role_id = ?)
如果坚持用递归查子菜单(比如用 withRecursive),在高并发或深层级菜单下容易触发 MySQL 临时表限制或超时,不推荐生产环境使用。
AuthRule 表设计要区分「菜单」和「操作」两类规则
很多项目把菜单路由和按钮权限混在同一个 auth_rule 表里,导致权限分配时无法精确控制:用户能看到菜单,但点进去提示“无权操作”。正确做法是用 type 字段区分:
-
type = 'menu':对应左侧导航项,condition可为空,status = 1表示启用该菜单入口 -
type = 'action':对应控制器方法或 API 接口,如admin/User/delete,需在中间件或基类中用Auth::check()校验
注意:Auth::check() 默认只认 name 字段,如果你把菜单 URL 存在 url 字段里,就得重写 check 方法或提前把 url 映射为规则名。
分配权限时必须同步刷新用户菜单缓存
ThinkPHP 的 Auth 类默认缓存权限规则,但不会自动感知菜单结构变更。常见错误是:后台改了菜单父子关系,或给角色分配了新菜单,前端还是旧菜单——因为 Auth::getRuleList() 返回的是缓存结果,且缓存 key 不含菜单树版本号。
解决办法只有两个:
- 每次修改菜单或权限后,手动清空
auth_rule相关缓存:Cache::delete('auth_rule_' . $roleId) - 在分配权限接口末尾,主动调用
Auth::init($roleId)强制重载规则(适用于单角色场景)
别依赖「等缓存自然过期」,用户可能等几分钟都看不到新菜单。
前端渲染菜单时别直接遍历全部菜单数据
后端传给前端的菜单数组,看起来是树形结构,但如果没过滤,会把所有菜单(包括未授权的)都塞进去,靠前端 JS 隐藏 —— 这是严重安全隐患。正确流程是:后端根据用户角色 + auth_rule 表,只返回「已授权且 status=1」的菜单节点,并确保子节点也满足权限条件(即父节点有权限 ≠ 子节点自动有权限,必须逐层 check)。
示例逻辑片段(在菜单服务类中):
$menus = Db::name('menu')->where('status', 1)->select();
$allowed = [];
foreach ($menus as $m) {
if (Auth::check($m['name'], $uid)) { // $m['name'] 是规则名,如 'admin/article/index'
$allowed[] = $m;
}
}
// 再用 $allowed 构建树(非原始全量菜单)
这个过程不能省,否则菜单折叠展开、面包屑生成都会出错,而且容易暴露未授权接口路径。
树状权限真正的复杂点不在组装,而在「权限继承」的语义定义:父菜单可访问是否意味着子菜单自动可访问?ThinkPHP 的 Auth 不处理这层逻辑,得自己在 check 方法里加 LIKE 判断或解析 path 做前缀匹配。这点最容易被忽略,结果就是管理员点了子菜单报 403,反复查路由配置却找不到原因。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











