第三章 RBAC 实现:数据库表设计
这一章我们动手把 RBAC 装进数据库里。嗯,说「装」其实有点轻巧,真正落地时你会发现表结构没设计好,后面加个角色都得改一堆代码。我在好几个项目里都吃过这个亏,所以今天把我的标准做法直接端出来。
3.1 核心表结构概览
RBAC 的经典模型只有五张表,但很多人把它想复杂了。说白了就是:用户、角色、权限,再加两张关联表把它们串起来。我习惯把这套结构叫做「三主两联」。
3.2 用户表
用户表是最基础的一张表,记录系统里每一个账号。我的经验是:不要把业务字段(比如部门、工号)直接塞进用户表,RBAC 只关心身份标识和启用状态,其他信息应该拆到用户扩展表里。
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(64) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(256) NOT NULL COMMENT '加密后的密码',
real_name VARCHAR(64) DEFAULT '' COMMENT '真实姓名',
email VARCHAR(128) DEFAULT '' COMMENT '邮箱',
phone VARCHAR(20) DEFAULT '' COMMENT '手机号',
avatar VARCHAR(256) DEFAULT '' COMMENT '头像路径',
enabled TINYINT(1) DEFAULT 1 COMMENT '启用状态:1-启用,0-禁用',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
3.3 角色表
角色就是一个权限组。比如「管理员」「运营」「只读用户」都是角色。角色表的核心是 role_key 字段,代码里用它做鉴权判断,而不是用中文名称。
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '角色ID',
role_name VARCHAR(64) NOT NULL COMMENT '角色名称(中文显示用)',
role_key VARCHAR(64) NOT NULL UNIQUE COMMENT '角色标识(代码中用)',
sort INT DEFAULT 0 COMMENT '排序号',
status TINYINT(1) DEFAULT 1 COMMENT '状态:1-正常,0-停用',
remark VARCHAR(256) DEFAULT '' COMMENT '备注',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
if(roleName == "管理员"),后来改角色名称时气得想打自己。用 role_key 判断,比如 admin、operator,改中文名不会影响逻辑。3.4 权限表
权限表的设计最容易出分歧。小项目可以按「菜单 + 按钮」粗暴处理,但上了规模一定要拆分。我的习惯是把权限分为三类:模块级、操作级、数据级。这张表只存模块和操作,数据级权限单独用规则表处理。
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '权限ID',
perm_name VARCHAR(64) NOT NULL COMMENT '权限名称',
perm_code VARCHAR(128) NOT NULL UNIQUE COMMENT '权限标识(如 system:user:add)',
type TINYINT(1) NOT NULL COMMENT '类型:1-模块,2-菜单,3-按钮',
parent_id BIGINT DEFAULT 0 COMMENT '父权限ID',
path VARCHAR(256) DEFAULT '' COMMENT '前端路由路径',
component VARCHAR(256) DEFAULT '' COMMENT '前端组件',
icon VARCHAR(64) DEFAULT '' COMMENT '图标',
sort INT DEFAULT 0 COMMENT '排序号',
status TINYINT(1) DEFAULT 1 COMMENT '状态',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';
perm_code 命名方式:模块:子模块:操作。例如 system:user:add、system:user:edit、order:view。这样拆开的好处是,后期可以做细粒度的按钮级权限控制。3.5 用户角色关联表
用户和角色是多对多关系。一个用户可以有多个角色,一个角色也可以属于多个用户。这张表只存两个外键,不要加其他业务字段。
CREATE TABLE sys_user_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '关联ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_role (user_id, role_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
UNIQUE KEY 确保不重复。有些人直接用两个字段做联合主键也行,但我习惯单独一个自增 id,方便以后做批量删除和游标分页。3.6 角色权限关联表
角色和权限也是多对多。同样很简单:角色 ID + 权限 ID。
CREATE TABLE sys_role_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '关联ID',
role_id BIGINT NOT NULL COMMENT '角色ID',
permission_id BIGINT NOT NULL COMMENT '权限ID',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_role_perm (role_id, permission_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';
3.7 表关系总览
| 表名 | 作用 | 关联关系 |
|---|---|---|
| sys_user | 存储账号和身份信息 | 通过 sys_user_role 关联角色 |
| sys_role | 定义角色分组 | 通过 sys_role_permission 关联权限 |
| sys_permission | 定义可操作的功能点 | 被角色引用 |
| sys_user_role | 用户与角色的关系 | 多对多桥接 |
| sys_role_permission | 角色与权限的关系 | 多对多桥接 |
3.8 建表顺序与外键注意事项
我建议先建主表(用户、角色、权限),再建关联表。外键不是必须的——说实话,我在生产环境里很少用物理外键。为什么?
- 性能问题:高并发下外键约束会增加检查开销。
- 分库分表困难:外键只能单库生效。
- 业务复杂度:删除角色时,你可以选择「先删关联再删角色」,而不是依赖数据库级联。
3.9 索引优化建议
查权限时最频繁的操作是:根据用户 ID 查他的角色列表,再根据角色查权限列表。所以关联表的 user_id 和 role_id 一定要加索引。上面建表时已经用了联合唯一索引,MySQL 会自动为第一列创建索引,但最好再单独给 role_id 加一个索引,因为角色权限关联表也经常按角色查。
-- 额外索引(可选)
ALTER TABLE sys_user_role ADD INDEX idx_user_role_role_id (role_id);
ALTER TABLE sys_role_permission ADD INDEX idx_role_perm_permission_id (permission_id);
3.10 快速使用案例
写完表当然要跑一遍。下面是一段插入测试数据的 SQL,你可以直接复制去验证。
-- 插入用户
INSERT INTO sys_user (username, password, real_name, enabled) VALUES
('zhangsan', '加密后的密码', '张三', 1),
('lisi', '加密后的密码', '李四', 1);
-- 插入角色
INSERT INTO sys_role (role_name, role_key, sort) VALUES
('管理员', 'admin', 1),
('运营员', 'operator', 2);
-- 插入权限
INSERT INTO sys_permission (perm_name, perm_code, type, parent_id) VALUES
('用户管理', 'system:user', 1, 0),
('用户列表', 'system:user:list', 2, 1),
('新增用户', 'system:user:add', 3, 1);
-- 建立关联
INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1), (2, 2);
INSERT INTO sys_role_permission (role_id, permission_id) VALUES
(1, 1), (1, 2), (1, 3), -- 管理员拥有所有权限
(2, 1), (2, 2); -- 运营员只有列表权限
查某个用户的所有权限用这条 SQL(多表连查):
SELECT DISTINCT p.perm_code
FROM sys_user u
JOIN sys_user_role ur ON u.id = ur.user_id
JOIN sys_role_permission rp ON ur.role_id = rp.role_id
JOIN sys_permission p ON rp.permission_id = p.id
WHERE u.username = 'zhangsan';
3.11 扩展思路:租户隔离
如果你做的是 SaaS 产品,每张表加一个 tenant_id 字段就行。我习惯把 tenant_id 放主表,关联表通过主表继承租户信息,这样查询时只需要过滤用户表就可以隔离整个权限数据。
好啦,表设计到这里就够用了。后面章节你会看到怎么用这些表写出实际的鉴权代码——到时候你会发现,只要表结构稳了,逻辑层就是顺手的事。嗯,今天就到这,动手建张表试试吧。