第 6 章 权限计算与生效:菜单、策略与缓存
本文导航

第 6 章 权限计算与生效:菜单、策略与缓存
前两章讲「怎么授」。这一章讲「授完之后系统怎么算」,以及为什么有时后台改了权限、页面却没变化。
我的菜单:GET /users/me/menus
Handler 逻辑:
app := strings.Join(ctx.QueryArray("app"), ",")
user, _ := ctx.Get("user")
var menus []*router.Menu
if user.(*model.User).IsSuperAdmin {
menus = i.menuRepo.GetMenuWithoutBtn()
} else {
menus = i.userSvc.GetUserMenus(ctx.Request.Context(), user.(*model.User).ID)
}
router.Success(ctx, router.FilterMenusBySubApp(menus, app))
分三步:
- 取菜单:
- 超管:全部菜单,去掉按钮;
- 普通用户:先查用户角色,再查角色菜单 ID,最后
GetMenuByMenuIDs。
- 过滤按钮:
GetMenuWithoutBtn/GetMenuByMenuIDs都会去掉MenuTypeBtn。 - 按子系统过滤:
app参数匹配根菜单的SubApp。
GetMenuByMenuIDs 还会把命中的菜单 Meta.ShowLink 置为 true,父节点因为子节点命中也会显示。
我的权限:GET /users/me/permissions
func (i *UserService) GetMyPermissions(ctx context.Context, userID uint, isSuperAdmin bool) ([]string, error) {
if isSuperAdmin {
return []string{"*:*:*"}, nil
}
user, err := i.userRepo.WithContext(ctx).FindById(userID)
if err != nil {
return nil, err
}
roleIDs := user.GetRoleIDs()
menuIDs := i.roleSvc.GetRolesMenuIDs(ctx, roleIDs...)
return i.menuManger.GetPermissionsByMenuIDs(menuIDs...), nil
}
普通用户的权限不是查 Casbin,而是从菜单重新推导:
用户 → 角色 ID → 角色菜单 ID → 菜单上的 DependentsPermission → 去重
所以 me/permissions 返回的是「按当前菜单配置,你应该有的权限」,而接口鉴权查的是 Casbin 策略。两者正常情况下一致;如果不一致,说明策略没有同步,见下一节。
策略什么时候写入
只有两个事件会写 casbin_rule:
| 事件 | 触发接口 | 写入 |
|---|---|---|
AssignMenuToRole |
PUT /roles/:id/menus |
删除角色旧 p 行,写入新 p 行 |
AssignRoleToUser |
POST /users/:id/roles |
删除用户旧 g 行,写入新 g 行 |
监听器在 owl-admin/app/listener/listener.go。
注意:
- 直接改数据库里的
admin_role_menu不会自动更新 Casbin; - 直接改
admin_user_role也不会; - 必须走接口,或手工同步
casbin_rule。
运行时怎么校验
中间件每次请求都会:
enforcer.LoadPolicy()
can, err := enforcer.Enforce(cast.ToString(user.ID), permissionKey)
LoadPolicy() 从数据库重新加载策略,所以后台授权后,下一次请求就生效,不需要重启服务。
代价是每次请求都读一次策略表。当前实现如此,量级不大时没问题;高并发场景可以改成定时刷新或监听变更。
前端缓存在哪里
前端登录后会调用 /users/me/permissions,把结果写进 Pinia 和 localStorage:
useUserStore().permissions;user-permissions-cache,TTL 24 小时;user-info里也有一份。
所以可能出现:
后端已经授予新权限 → 接口可以访问
前端按钮仍然不显示(缓存还是旧的)
处理方式:
- 重新登录;
- 或调用
useUserStore().fetchAndCachePermissions()刷新; - 清掉浏览器 localStorage 里的
user-permissions-cache。
一次授权的完整生效时间线
T0 管理员给角色勾选「创建用户」
T0 后端写 admin_role_menu,发布事件,写 casbin p 行
T1 用户下一次请求 POST /users
T1 中间件 LoadPolicy + Enforce,通过
T2 前端刷新权限缓存,按钮出现
接口权限在 T1 生效;页面按钮在 T2 生效。
常用排查 SQL
查某角色的策略:
SELECT * FROM casbin_rule WHERE ptype = 'p' AND v0 = '<roleID>';
查某用户的分组:
SELECT * FROM casbin_rule WHERE ptype = 'g' AND v0 = '<userID>';
查角色菜单:
SELECT * FROM admin_role_menu WHERE role_id = <roleID>;
查用户角色:
SELECT * FROM admin_user_role WHERE user_id = <userID>;
对不上的常见原因
me/permissions 有,接口仍 403
- Casbin 没有对应
p行:角色分配菜单的接口没走,或事件监听失败; g行缺失:用户没有分配角色,或分配后事件没执行;- 用户 ID 类型不一致:
g行里存的是十进制字符串,确认没有写成别的格式。
接口能访问,me/permissions 没有
- 权限来自另一个角色,但前端缓存是旧的;
- 或者该接口是
AccessAuthenticated/AccessSuperAdmin,根本不需要权限标识。
改了菜单,策略没变
- 只改了
admin_role_menu表,没有走PUT /roles/:id/menus; - 事件总线没初始化(
listener.Init未执行),检查Bootstrap()。
自检
- [ ] 能说清「我的菜单」和「我的权限」的计算路径;
- [ ] 知道
p行和g行分别由哪个接口写入; - [ ] 知道后端鉴权每次请求都
LoadPolicy; - [ ] 知道前端权限有 24 小时缓存;
- [ ] 能用 SQL 定位策略缺失。
评论
加载评论中…