RBAC权限数据通过permission_code字段与菜单节点严格映射,后端一次性查询全量菜单构建树结构,并依据用户权限集裁剪:父节点permission_code为空时保留,子节点仅当自身或父节点有权限才纳入,最终返回JSON菜单树供前端动态渲染。

RBAC 权限数据如何映射到菜单结构
菜单动态渲染的前提不是“有权限就能显示”,而是“权限声明必须能反向推导出可见菜单节点”。Go 后端通常不直接生成 HTML,而是提供 /api/menus 接口返回 JSON 格式菜单树,前端据此渲染。关键在于:权限标识(如 "user:list"、"order:export")必须与菜单项的 permission_code 字段严格对应,且后端需在构建菜单时做权限裁剪。
常见错误是把角色 ID 直接塞进菜单响应,或让前端靠角色名(如 "admin")硬编码判断——这会导致权限变更后菜单无法自动收敛。
- 菜单表建议含字段:
id、name、path、parent_id、permission_code(非空,唯一标识该菜单所需权限) - 用户登录后,后端查出其所有有效
permission_code(去重、去无效项),再递归加载菜单树,对每个节点执行if contains(userPerms, menu.PermissionCode) - 注意:父菜单是否显示,不能只看自身权限;若子菜单有权限而父菜单无
permission_code(即纯分组菜单),需保留父节点——此时应设permission_code = ""并在裁剪逻辑中特殊处理
用 Go 构建带权限过滤的菜单树
别写递归查询 N+1。用一次 SQL 查全量菜单,再用 map 建立父子关系,最后遍历裁剪。核心是避免在循环中查数据库或重复切片操作。
示例裁剪逻辑(简化版):
// menus 是从 DB 查询出的 []Menu,已按 parent_id 排序
menuMap := make(map[uint]*Menu)
for i := range menus {
menuMap[menus[i].ID] = &menus[i]
}
var rootMenus []*Menu
for i := range menus {
m := &menus[i]
if m.ParentID == 0 {
if m.PermissionCode == "" || userHasPerm(m.PermissionCode) {
rootMenus = append(rootMenus, m)
}
} else if parent, ok := menuMap[m.ParentID]; ok {
if parent.PermissionCode == "" || userHasPerm(parent.PermissionCode) {
// 父节点已确定可见,才考虑加子节点
if m.PermissionCode == "" || userHasPerm(m.PermissionCode) {
parent.Children = append(parent.Children, m)
}
}
}
}
-
userHasPerm应是 O(1) 查 map,不是每次调 SQL 或 Redis - 菜单结构体必须预定义
Children []*Menu字段,不要依赖 ORM 自动关联 - 若菜单含多语言字段(如
name_zh、name_en),应在查询时根据请求头Accept-Language过滤,而非返回全部字段让前端判别
前端传来的 role_id 或 token 怎么安全校验
不要在接口里接收 role_id 并以此查菜单——这是典型越权漏洞。RBAC 的校验起点必须是当前请求的认证凭证(如 JWT),从中解析出用户 ID,再查该用户所拥有的权限集。
- JWT payload 中只放
user_id和exp,绝不放role_name或permissions数组(易被篡改) - 中间件中用
ctx.Value("user_id")透传用户身份,菜单 handler 再基于此查缓存或 DB - 权限缓存建议用
redis.Set("user:perms:{uid}", []string{"a:b", "c:d"}, time.Hour),避免每次请求都查关联表 - 如果用了 Casbin,调
e.GetFilteredPolicy(0, userID)拿权限,但注意它返回的是二维字符串 slice,需转成map[string]struct{}才适合高频判断
为什么菜单接口要区分「路由级」和「按钮级」权限
一个 /users 页面可能需要 "user:list" 路由权限,而页面内的「导出」按钮需要额外的 "user:export" 按钮权限。如果菜单接口只返回第一层,按钮权限就得前端靠角色名硬编码,或者另发请求——这破坏一致性。
更合理的做法是在菜单响应中为每个节点附加 actions 字段:
{
"name": "用户管理",
"path": "/users",
"permission_code": "user:list",
"actions": ["user:create", "user:export", "user:delete"]
}
- 前端渲染菜单时,只用
permission_code控制是否显示该菜单项 - 进入页面后,用
actions数组控制按钮显隐或禁用状态,无需额外请求 - 后端在构建菜单时,把该菜单下所有关联的按钮权限(从权限规则表 or Casbin policy 表中查)一并注入
actions,而不是让前端猜 - 注意:按钮权限不参与菜单树裁剪,只作为元数据下发;否则会导致「有列表权限但没导出按钮」却无法发现配置缺失
菜单动态渲染最易被忽略的点,是把权限校验逻辑分散在数据库查询、中间件、模板渲染多个环节。真正健壮的做法,是把权限裁剪收束在菜单构建这一层,输入是原始菜单 + 用户权限集合,输出是完全裁剪后的树结构——其余地方只管消费,不许二次判断。


















