权限模型应拆分为用户、角色、菜单、按钮四实体并用中间表关联;RolePermission表仅存RoleId、ResourceType、ResourceId;按钮权限校验用内存HashSet缓存ButtonCode;菜单渲染需递归过滤无权子树;JWT中不存权限字段,改用版本号机制同步缓存。

权限数据模型怎么建才不翻车
角色和菜单权限混在一个表里,后期改需求时字段爆炸是大概率事件。必须拆开:用户、角色、菜单、按钮四个实体,用中间表关联。
常见错误是把按钮权限直接挂在菜单上,结果一个菜单有 5 个按钮,每次增删都要改菜单记录——实际应该让 RolePermission 表只存 RoleId、ResourceType(比如 "menu" 或 "button")、ResourceId 三字段,资源类型和 ID 分离。
-
Menu表存路径("/user/list")、名称、父级 ID、排序,不存权限逻辑 -
Button表单独存按钮标识(如"user:delete")、所属菜单 ID、显示文本 -
RolePermission表用联合唯一索引约束(RoleId, ResourceType, ResourceId),避免重复授权
运行时怎么高效校验按钮权限
每次点击按钮都查一次数据库?响应延迟会明显。得缓存,但不能缓整个角色权限树——按钮粒度太细,缓存体积大且更新难。
推荐方案:登录后加载该用户所有有效 ButtonCode(如 "user:edit"、"order:export")进内存集合,用 HashSet 存。前端按钮渲染或后端接口拦截时,直接 .Contains() 判断。
- 缓存键建议用
$"permissions:{userId}",过期时间设为 30 分钟,配合登录态刷新 - 按钮码命名统一用冒号分隔:
模块:操作,避免空格或斜杠导致解析歧义 - 别在 Controller 里手动写
if (!HasPermission("user:delete")) throw ...——抽成[Permission("user:delete")]特性,用 ActionFilter 拦截
菜单动态渲染为什么总漏项或错序
前端拿到菜单列表,发现有的没显示、有的顺序乱、有的不该出现的出现了——八成是后端没做“可访问菜单”过滤,或者父子关系没递归处理好。
关键不是拼 JSON,而是从根菜单出发,逐层判断当前用户是否有任一子资源权限(菜单本身或其任意按钮)。没权限的整棵子树就剪掉。
- SQL 查询别用多次 JOIN 拼全树,容易因权限缺失导致父菜单也被连带过滤;改用 CTE 递归查出所有菜单 ID,再用
IN批量查权限状态 - 后端返回菜单结构前,务必按
SortOrder字段升序排列,别依赖前端排序——不同浏览器对 undefined 排序行为不一致 - 如果菜单支持多语言,
Name字段别存在Menu表里,应放MenuLocalization表,按CultureInfo查
JWT Token 里塞权限字段行不行
能省一次数据库查询,但非常危险:Token 一旦签发就不可撤回,用户权限变更后旧 Token 还能继续用,直到过期。
除非是超短时效(比如 5 分钟),否则别把按钮码塞进 JWT 的 claims。更稳妥的做法是 Token 只存 RoleId 和 UserId,权限校验走内存缓存 + 异步更新机制。
- Token 中加
"perm_ver"版本号字段,每次权限变更时自增,校验时比对缓存中的版本,不一致就强制刷新权限缓存 - 别用
HttpContext.User.Claims直接读按钮码——它来自 Token,不可信;必须以缓存或 DB 结果为准 - 调试时注意:Swagger 或 Postman 发请求带的 Token 如果过期,
Authorization头可能被静默忽略,导致权限校验永远走默认分支
菜单和按钮权限看着只是“显不显示”的问题,但权限判断点一旦分散在前端路由、后端接口、按钮组件、导出逻辑里,漏一处就等于留个后门。最麻烦的不是实现,是确保所有入口都走同一套校验路径,且缓存失效时机和数据库变更严格对齐。











