给每个用户单独配权限,几百号人之后必然失控。RBAC 用”角色”把用户和权限解耦,是中后台权限系统的事实标准。本文讲透 RBAC 的三表五表设计,角色继承、数据权限等扩展形态,按钮级、接口级、行级权限的落地方式,以及网关与服务两层校验各自该管什么。

它解决什么问题

不引入角色的直接授权,管理规模是”用户数 × 权限点数”:一千个用户、两百个权限点,就是二十万条授权关系,人员入职、调岗、离职每一次变动都要逐条增删,漏一条就是一个越权或权限缺失。

实际系统里还有更细的诉求:同一页面,有人能看到”导出”按钮有人看不到;同一个接口,有人能查全部订单有人只能查本部门的;前端把按钮藏掉之后,直接调接口的人依然畅通无阻。权限系统设计要同时回答四个问题:权限怎么建模、粒度怎么划分、校验在哪一层执行、数据范围怎么约束。

核心原理

RBAC 基本模型

RBAC(Role-Based Access Control,基于角色的访问控制)的核心思想是:用户不直接持有权限,而是挂到角色上,角色聚合权限。用户与角色、角色与权限都是多对多关系。这样权限调整收敛为”改角色定义”,该角色下所有用户批量生效;人员变动只需要换角色,不动权限点本身。

落到数据库是经典的三张主表加两张关联表:

CREATE TABLE `sys_user` (
`id` BIGINT PRIMARY KEY,
`username` VARCHAR(64) NOT NULL,
`dept_id` BIGINT
);
CREATE TABLE `sys_role` (
`id` BIGINT PRIMARY KEY,
`code` VARCHAR(64) NOT NULL,
`name` VARCHAR(64) NOT NULL
);
CREATE TABLE `sys_permission` (
`id` BIGINT PRIMARY KEY,
`code` VARCHAR(128) NOT NULL,
`type` VARCHAR(16) NOT NULL, -- menu / button / api / data
`name` VARCHAR(64) NOT NULL
);
CREATE TABLE `sys_user_role` (
`user_id` BIGINT NOT NULL,
`role_id` BIGINT NOT NULL,
PRIMARY KEY (`user_id`, `role_id`)
);
CREATE TABLE `sys_role_permission` (
`role_id` BIGINT NOT NULL,
`permission_id` BIGINT NOT NULL,
PRIMARY KEY (`role_id`, `permission_id`)
);

查出某个用户的全部权限码,一条三表 JOIN 即可:

SELECT DISTINCT p.code
FROM sys_permission p
JOIN sys_role_permission rp ON rp.permission_id = p.id
JOIN sys_user_role ur ON ur.role_id = rp.role_id
WHERE ur.user_id = #{userId};

登录成功后执行这个查询,把权限码集合缓存进会话或 Redis,后续每次请求的校验只读缓存,不打数据库。

深入机制

RBAC 的扩展形态

标准 RBAC(常称 RBAC0)够用但不够灵活,实践中常见这些扩展:

  • 角色继承(RBAC1):角色之间建立父子关系,”部门经理”继承”普通员工”的全部权限再追加自己的。好处是公共权限只配一份;代价是继承层级深了排查困难,工程上一般限制在两三层以内。
  • 用户直授权限:标准模型要求”给权限必须先造角色”,临时给一个人加一个权限也得建角色,效率低。不少系统允许用户权限 = 角色带来的权限 ∪ 直接授予的权限,用一点规范性换灵活性。
  • 用户组与组织维度:把角色绑定到用户组或部门节点上,整组授权,适合按组织架构批量管理。
  • 约束规则(RBAC2/RBAC3):互斥角色(如出纳与会计不得同为一人)、角色人数上限、先决角色,用来落实职责分离,金融、财务类系统尤其需要。

按钮级与接口级权限

前提是先设计稳定的权限码,推荐 系统:模块:资源:动作 的分层格式,例如 order:list:export、system:user:add。权限码只表达语义,不绑定菜单名称,菜单改名字、挪位置都不影响校验逻辑。

按钮级:用户登录后,前端拉取权限码集合,封装成指令或高阶组件(如 v-permission="'order:list:export'"),无权限的按钮不渲染或置灰。这只是体验优化。

接口级:服务端对每个 API 声明所需权限码,通过注解加 AOP、或拦截器统一处理:

@PreAuthorize("hasAuthority('order:list:export')")
@GetMapping("/orders/export")
public void exportOrders() { ... }

校验失败默认拒绝。铁律只有一条:前端隐藏永远不能代替后端检查,也不能信任客户端传入的 userId、deptId、资源 ID,一切归属判断以服务端会话里的身份为准。

数据行级权限

功能权限回答”能不能操作”,数据权限回答”能操作哪些行”。典型规则枚举:全部数据 / 本部门及以下 / 本部门 / 仅本人 / 自定义部门集合。规则配置在角色上,查询时由数据访问层动态拼接过滤条件。MyBatis 体系下常用拦截器改写 SQL:

-- 原始 SQL
SELECT * FROM t_order WHERE status = 1;

-- 数据范围为"本部门及以下"时被改写为
SELECT * FROM t_order WHERE status = 1
AND dept_id IN (
SELECT dept_id FROM sys_dept
WHERE FIND_IN_SET(#{currentDeptId}, ancestors)
);

落地注意三点:部门树用 ancestors 路径或闭包表存储,避免递归查询;自定义范围需要额外的”角色-部门”关联表;超级管理员直接放行跳过拼接。改写后的条件要确认走得上索引,大范围 IN 子查询可能把查询拖成全表扫描。

网关与服务的分层校验

微服务架构下,权限校验通常分两层,职责不同:

  • 网关层做粗粒度的事:验证 Token 有效性、查黑名单、按路由规则拦截(如 /admin/** 要求管理员角色)、限流防刷。它拦的是”明显不该进来的流量”。
  • 服务层做细粒度的事:接口权限码校验、数据行级过滤、业务规则(订单必须属于本人才能取消)。它需要业务上下文,只有服务自己拿得到。

为什么网关验过了服务还要验?三个理由:网关不了解业务数据,做不了行级判断;服务间的内网调用可以绕过网关直达;纵深防御要求任何单层失守仍有兜底——安全校验默认假设上游不可信。

工程实践

  • 权限码集中定义为常量或枚举,随代码评审变更;菜单、按钮、接口共用同一套码。
  • 权限码集合缓存进登录态,角色变更时主动失效对应用户的缓存,或给缓存带版本号加短 TTL,控制生效延迟。
  • 治理角色爆炸:提供默认角色(超管、管理员、普通成员)覆盖八成场景,临时需求走”申请—审批—生效—到期回收”流程,而不是随手建角色。
  • 授权变更必须落审计日志:谁在什么时间给谁授了什么权限,支持按人、按角色反查。
  • 需要”工作日上午九点前禁止访问””金额超十万要求更高角色”这类上下文规则时,RBAC 表达不了,应引入 ABAC 按属性动态决策;但 ABAC 的策略解释和排错成本高得多,普通中后台用 RBAC 加少量硬编码规则更务实。

对比与权衡

权限类型 粒度 校验位置 实现方式 前端隐藏可替代吗
菜单/页面 路由级 前端 + 网关可选 按权限码动态生成路由 否,直接输 URL 仍可到达前端页面
按钮 页面元素 前端 指令/组件控制显隐 否,必须配合接口校验
接口 API 级 服务端 注解 + 拦截器,默认拒绝 无关,这是最后防线
数据行 记录级 服务端数据访问层 范围规则 + SQL 改写 无关

粒度越细越安全,但配置和排查成本同步上升,权限点数量失控后角色设计会退化成”一人一角”。实践中先把接口级和数据行级做扎实,菜单与按钮权限只当体验层看待。

面试视角

RBAC 需要几张表?怎么查一个用户的权限? 三张主表(用户、角色、权限)加两张关联表(用户-角色、角色-权限),共五张。查询时用权限表 JOIN 两张关联表,按用户 ID 过滤并去重,登录时加载进缓存供后续校验使用。

按钮级权限怎么做才安全? 登录后前端拉取权限码集合,用指令控制按钮显隐;关键是服务端对应接口必须独立声明权限码并强制校验。只藏按钮不验接口,等于没做。

“只能看本部门数据”怎么实现? 在角色上配置数据范围规则(全部/本部门及以下/仅本人等),数据访问层用拦截器按规则改写查询的 WHERE 条件;部门树配合路径字段或闭包表查询,超级管理员直接放行。

网关已经鉴权了,服务里还要再校验吗? 要。网关没有业务上下文,做不了行级和对象级判断;内网服务间调用可能不经过网关;纵深防御原则下每一层都要默认上游不可信。

RBAC 有什么局限? 它只能表达”某人是什么角色”这类静态关系,无法描述基于资源属性、时间、地点等上下文的动态规则,那是 ABAC 的领域;另外粒度把控不好容易产生角色爆炸,需要默认角色、申请审批和定期清理来治理。

小结

  • RBAC 的本质是用角色把”用户—权限”的多对多爆炸收敛为两段可管理的多对多。
  • 五张表撑起核心模型,角色继承、用户直授、互斥约束是按需叠加的扩展,不是必选项。
  • 权限码要稳定、分层、与菜单名解耦;按钮级管体验,接口级才是安全边界。
  • 行级数据权限 = 角色上的范围规则 + 数据访问层的 SQL 改写,别让大范围 IN 拖垮索引。
  • 网关管认证和粗粒度,服务管细粒度与数据范围,两层各有不可替代的职责。