第4章:角色与权限模型设计:RBAC0模型实现

权限设计,说白了就是解决「谁能干什么」的问题。

我见过不少初创团队,一开始图省事,直接在用户表里加个 role 字段,用 0、1、2 表示不同角色。结果呢?业务一扩张,权限逻辑改得想哭。嗯,今天我们就聊聊 RBAC0 模型,这是最经典、最实用的权限模型。

4.1 为什么是 RBAC0?

RBAC 全称是 Role-Based Access Control,基于角色的访问控制。RBAC0 是核心模型,它定义了三个基本元素:用户、角色、权限。

你想想看,如果每个用户都直接绑定权限,那管理起来有多恐怖?100 个用户,每个用户 10 个权限,就是 1000 条记录。如果某个权限要调整,你得遍历所有用户。我曾经接手过一个老系统,就是这种设计,改一个权限字段要跑半小时 SQL,别提多痛苦了。

RBAC0 的思路很简单:用户不直接关联权限,而是通过角色来间接关联。用户属于某个角色,角色拥有某些权限。这样,你只需要维护角色和权限的关系,用户那边自动继承。

核心公式:用户 → 角色 → 权限

用户与角色是多对多关系,角色与权限也是多对多关系。

4.2 数据库表结构设计

我们来看具体的表设计。一共需要五张核心表:

表名 说明 核心字段
sys_user 用户表 user_id, username, password, status
sys_role 角色表 role_id, role_name, role_code, status
sys_user_role 用户-角色关联表 id, user_id, role_id
sys_menu 菜单权限表 menu_id, parent_id, menu_name, path, component, perms, icon, sort, visible
sys_role_menu 角色-菜单关联表 id, role_id, menu_id

这里我重点说一下 sys_menu 表。它不只是存菜单,还存了按钮权限。你看 perms 这个字段,它存的是权限标识符,比如 system:user:addsystem:user:edit。前端根据这个标识符来控制按钮的显示隐藏。

-- 菜单权限表建表语句
CREATE TABLE sys_menu (
    menu_id     BIGINT       NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
    parent_id   BIGINT       DEFAULT 0 COMMENT '父菜单ID',
    menu_name   VARCHAR(50)  NOT NULL COMMENT '菜单名称',
    path        VARCHAR(200) DEFAULT '' COMMENT '路由路径',
    component   VARCHAR(255) DEFAULT NULL COMMENT '组件路径',
    perms       VARCHAR(100) DEFAULT NULL COMMENT '权限标识',
    icon        VARCHAR(50)  DEFAULT '#' COMMENT '菜单图标',
    sort        INT          DEFAULT 0 COMMENT '排序',
    visible     TINYINT      DEFAULT 1 COMMENT '是否可见(0隐藏 1显示)',
    menu_type   CHAR(1)      DEFAULT 'M' COMMENT '菜单类型(M目录 C菜单 F按钮)',
    status      TINYINT      DEFAULT 1 COMMENT '状态(0禁用 1启用)',
    create_time DATETIME     DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (menu_id)
) COMMENT '菜单权限表';

我的习惯:menu_type 字段用 M、C、F 来区分类型。M 是目录(只有子菜单),C 是菜单(可点击跳转),F 是按钮(没有页面,只有权限标识)。这样前端渲染菜单树时,逻辑非常清晰。

4.3 按钮权限表(permission)

有些团队会把按钮权限单独拆一张表,叫 sys_permission。我个人觉得,如果项目不大,直接复用 sys_menu 表的 perms 字段就够了。但如果你做的是大型企业级应用,比如 ERP、CRM 这类,我建议还是拆开。

-- 按钮权限表(独立方案)
CREATE TABLE sys_permission (
    permission_id   BIGINT       NOT NULL AUTO_INCREMENT COMMENT '权限ID',
    permission_name VARCHAR(50)  NOT NULL COMMENT '权限名称',
    permission_code VARCHAR(100) NOT NULL COMMENT '权限编码',
    menu_id         BIGINT       DEFAULT NULL COMMENT '所属菜单ID',
    status          TINYINT      DEFAULT 1 COMMENT '状态',
    create_time     DATETIME     DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (permission_id),
    UNIQUE KEY uk_code (permission_code)
) COMMENT '按钮权限表';

-- 角色-权限关联表
CREATE TABLE sys_role_permission (
    id              BIGINT NOT NULL AUTO_INCREMENT,
    role_id         BIGINT NOT NULL,
    permission_id   BIGINT NOT NULL,
    PRIMARY KEY (id)
) COMMENT '角色权限关联表';

为什么要拆?因为有些权限不绑定菜单。比如「导出报表」这个按钮,它可能出现在多个页面,但权限逻辑是统一的。拆出来之后,角色直接关联 permission_id,维护起来更灵活。

避坑指南:我曾经遇到过一个项目,权限标识符命名混乱,有的用下划线,有的用驼峰,有的用点号。后来统一规范为 模块:功能:操作 的格式,比如 order:export:excel。建议你从一开始就定好命名规范,不然后面改起来很麻烦。

4.4 数据权限隔离方案

数据权限,说白了就是「同一张表,不同角色看到的数据不一样」。比如销售经理能看到所有销售订单,普通销售只能看到自己的订单。

数据权限的实现,一般有几种方案:

  1. 硬编码方案:在 SQL 查询中直接加 WHERE 条件。比如 WHERE create_by = 当前用户ID。简单粗暴,但维护成本高。
  2. 注解 + AOP 方案:在 Mapper 方法上加注解,通过 AOP 动态拼接 SQL。比如 @DataScope(deptAlias = "d"),自动拼接部门过滤条件。
  3. 数据权限规则表:设计一张数据权限规则表,存储每个角色对应的过滤条件。

我个人比较推荐第二种方案,也是目前主流框架(如若依、Spring Security)的做法。来看一个简单的数据权限注解设计:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataScope {
    /**
     * 部门表的别名
     */
    String deptAlias() default "";

    /**
     * 用户表的别名
     */
    String userAlias() default "";

    /**
     * 权限字符(用于匹配角色)
     */
    String permission() default "";
}

然后在 AOP 切面中,根据当前用户的角色,动态拼接 SQL。比如:

// 伪代码示例
if (用户角色 == "部门经理") {
    sql += " AND d.dept_id IN (子部门ID列表)";
} else if (用户角色 == "普通员工") {
    sql += " AND d.create_by = 当前用户ID";
}

数据权限的粒度:

  • 全部数据:超级管理员可见
  • 本部门及子部门数据:部门经理可见
  • 本部门数据:部门主管可见
  • 仅本人数据:普通员工可见
  • 自定义数据:通过规则表灵活配置

4.5 权限校验的完整流程

一个请求进来,权限校验的流程大概是这样的:

  1. 用户登录,后端生成 JWT Token,存入 Redis(包含用户ID、角色信息)
  2. 每次请求,拦截器解析 Token,获取当前用户信息
  3. 访问某个接口时,通过 @PreAuthorize 注解校验权限
  4. 校验逻辑:从 Redis 获取该用户的权限标识列表,判断是否包含当前接口所需的权限标识
  5. 如果是数据权限,再通过 AOP 动态拼接 SQL 过滤条件

这里有个细节:权限标识列表不要每次都查数据库。我习惯在用户登录时,把该用户所有角色的权限标识合并后,缓存到 Redis 里。缓存 key 可以设计为 auth:perms:用户ID,过期时间设为 2 小时。这样既保证了实时性,又减轻了数据库压力。

小技巧:如果角色权限变更了,记得清除该角色下所有用户的权限缓存。我一般会在修改角色权限的接口里,加一个「清除关联用户缓存」的操作。虽然多了一步,但能避免用户权限不同步的问题。

4.6 总结

RBAC0 模型,说白了就是三层结构:用户、角色、权限。它解决了权限管理中最核心的问题——灵活性和可维护性。

菜单权限控制「能看什么」,按钮权限控制「能点什么」,数据权限控制「能看到哪些数据」。这三层加在一起,基本能覆盖 90% 的企业级权限需求。

嗯,下一章我们会聊聊流程引擎的表设计,那是 OA 系统的另一个核心模块。到时候见。