后端如何设计 RBAC 权限管理系统

liulian
liulian 正式会员超兽战士
发布于 2026-10-07 16:45 ·1 浏览 ·0 回复

后端设计 RBAC 权限管理系统,结论是:用「用户—角色—权限」5 张核心表打底,权限用 资源:操作 字符串编码,鉴权走「注解声明 + 拦截器校验 + Redis 缓存权限集合」三件套,行级数据权限单独用一条 SQL 改写链路实现。这套结构能覆盖绝大多数后台管理系统的需求,用户量到百万级也不用换模型。

RBAC0、RBAC1、RBAC2、RBAC3 有什么区别,该选哪个?

结论:中小项目选 RBAC1(RBAC0 + 角色继承),不要一上来就做 RBAC3。

RBAC 是 Role-Based Access Control(基于角色的访问控制)的缩写,核心思想是「不把权限直接给用户,而是给角色,用户再挂角色」。按 NIST 标准分四级:

  • RBAC0:基础模型,用户、角色、权限、用户-角色、角色-权限,共 5 张表。
  • RBAC1:在 RBAC0 上加角色继承(角色有父角色,子角色继承父角色权限),多一张 role_parent 表。
  • RBAC2:加约束,比如「互斥角色不能同时授予同一用户」「一个用户最多 3 个角色」。
  • RBAC3:RBAC1 + RBAC2 的合并。

实践建议:90% 的系统用 RBAC0 就够;只有当出现「运营专员 / 运营主管 / 运营总监」这种明显的层级复用、导致大量重复授权时,才加角色继承。互斥约束(RBAC2)通常用业务代码硬判断即可,比如「财务审核人不能是制单人」,不值得为它建一套约束引擎。

RBAC 的表结构怎么设计?

结论:5 张核心表 + 1 张数据范围表,字段别贪多。

CREATE TABLE user (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(64) NOT NULL UNIQUE,
  password VARCHAR(128) NOT NULL
);

CREATE TABLE role (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  code VARCHAR(64) NOT NULL UNIQUE,   -- 如 SUPER_ADMIN、ORDER_MANAGER
  name VARCHAR(64) NOT NULL,
  data_scope TINYINT NOT NULL DEFAULT 4 -- 1全部 2本部门 3本部门及下级 4仅本人 5自定义
);

CREATE TABLE permission (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  code VARCHAR(128) NOT NULL UNIQUE,  -- 如 order:read、order:export
  name VARCHAR(64) NOT NULL,
  type TINYINT NOT NULL,              -- 1菜单 2按钮 3接口
  parent_id BIGINT DEFAULT 0
);

CREATE TABLE user_role (
  user_id BIGINT NOT NULL,
  role_id BIGINT NOT NULL,
  PRIMARY KEY (user_id, role_id)
);

CREATE TABLE role_permission (
  role_id BIGINT NOT NULL,
  permission_id BIGINT NOT NULL,
  PRIMARY KEY (role_id, permission_id)
);

关键点有三个:一是 permission.code 加唯一索引,鉴权时全靠它;二是两张关联表用联合主键,天然防重复;三是菜单树用 parent_id 自关联,不要单独再建一张菜单表,否则权限码会出现两套。

权限码怎么设计?

结论:用 资源:操作 两段式小写字符串,禁止用数字 ID 做鉴权判断。

推荐格式:order:read、order:export、user:reset-password。理由很直接——数字 ID 在日志、网关、前端按钮里都不可读,排查问题时得查库才知道 1024 是什么;而字符串能自解释,前端按钮的 v-permission="'order:export'" 直接写死,不用查接口。

粒度控制在「接口级」就够了,不要细到字段级。字段级通常只有「手机号脱敏」这类场景,用单独的脱敏注解处理,不要塞进 RBAC 权限码里。

接口鉴权代码怎么落地?

结论:注解 + 拦截器 + Redis 缓存,单次鉴权走内存 Set,耗时在 1 毫秒内。

四步:

  1. 接口上标 @RequiresPermission("order:export")。
  2. 拦截器解析注解,取当前登录用户 ID。
  3. 从 Redis 读 perm:user:{userId}(存的是 Set),SISMEMBER 判断权限码是否存在;未命中则查库回填,设 30 分钟 TTL。
  4. 命中放行,未命中抛 403。

超级管理员不要往 Redis 里写全量权限,直接在拦截器里判 role.code == 'SUPER_ADMIN' 短路放行,否则新增权限时需要批量刷新缓存。

用户登出、角色权限变更、给用户改角色这三类操作,必须主动 DEL perm:user:{userId}。多实例部署时用 Redis 的发布订阅广播失效事件,别只在本地清缓存。

角色爆炸和数据权限怎么解决?

结论:角色数超过 50 个就该考虑权限组,行级数据权限必须和功能权限分开做。

角色爆炸的典型信号是「每个新员工都要建一个新角色」。解法有两种:一是引入角色继承,把公共权限上提到父角色;二是拆出「权限组」(一组权限的集合),角色 = 若干权限组,配置量从 O(角色×权限) 降到 O(角色 + 权限组)。

数据权限解决的是「同一个 order:read 接口,A 只能看自己的订单,B 能看整个部门」。做法是给角色定 data_scope,再在 MyBatis 拦截器里按 scope 改写 SQL:scope=4 拼 AND create_by = {userId},scope=3 拼 AND dept_id IN (子部门ID集合),scope=1 不拼条件。核心原则:数据权限由后端 SQL 强制注入,绝不信任前端传的 deptId 参数。

最后收一下:5 张表定模型,资源:操作 定权限码,注解 + 拦截器 + Redis 定鉴权,data_scope 加 SQL 改写定行级权限,权限变更时主动清缓存。按这个顺序落地,前 3 天就能跑通一条完整链路,后续加权限只是加数据,不用动架构。

版权声明:本文来自 GJ站长论坛《后端如何设计 RBAC 权限管理系统》
原文链接:https://www.gj0.com/thread-420.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~