4. RBAC 数据库设计:用户表、角色表、权限表,关联关系与 SQL 示例
聊到权限系统,很多人第一反应就是「建几张表,存一下」。但实际上,表结构设计得好不好,直接决定了后续的扩展性和维护成本。
我个人习惯,在设计 RBAC 的数据库时,会遵循一个核心原则:职责分离。用户只管是谁,角色只管是什么岗位,权限只管能干什么。三者之间通过关联表解耦。
4.1 核心三张表长什么样?
先说最基础的三张表:users(用户表)、roles(角色表)、permissions(权限表)。
| 表名 | 核心字段 | 职责说明 |
|---|---|---|
| users | id, username, password_hash, status, created_at | 记录系统用户的基本信息。尽量不存冗余的权限相关字段。 |
| roles | id, role_name, role_code, description, is_system | 定义角色,比如「高级审批员」「部门经理」。role_code 用于代码中硬编码判断。 |
| permissions | id, perm_name, perm_code, resource, action, description | 定义具体的操作权限,比如「审批-通过」「订单-查看」。 |
一个小建议: 字段类型上,主键我用 BIGINT 自增,除非你需要分布式 ID。角色表和权限表的 code 字段建议设置唯一索引,这是你在代码里做权限判断的依据。
4.2 关联关系:多对多的桥梁
用户和角色之间,角色和权限之间,都是 多对多 关系。所以要引入两张关联表:user_role 和 role_permission。
你想想看,如果没有关联表,直接在用户表里加一个 role_id 字段,那一个用户只能有一个角色,这在真实业务中根本行不通。一个人可能同时是「运维」又是「审批员」。
核心关联关系:
users与user_role:一对多roles与user_role:一对多roles与role_permission:一对多permissions与role_permission:一对多
下面这张图,把四张表的关系串起来了。
4.3 SQL 建表示例(MySQL 方言)
纸上谈兵没意思,直接上 SQL。这是我个人比较喜欢的一套模板,加了些注释,方便团队新人理解。
-- 1. 用户表
CREATE TABLE `users` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` VARCHAR(64) NOT NULL COMMENT '登录名',
`password_hash` VARCHAR(255) NOT NULL COMMENT '加密密码',
`status` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '状态: 1启用, 0禁用',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 2. 角色表
CREATE TABLE `roles` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` VARCHAR(64) NOT NULL COMMENT '角色显示名称',
`role_code` VARCHAR(64) NOT NULL COMMENT '角色编码, 用于代码判断',
`description` VARCHAR(255) DEFAULT NULL COMMENT '角色描述',
`is_system` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否是系统内置角色',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_code` (`role_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
-- 3. 权限表
CREATE TABLE `permissions` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '权限ID',
`perm_name` VARCHAR(64) NOT NULL COMMENT '权限名称',
`perm_code` VARCHAR(128) NOT NULL COMMENT '权限编码, eg: order:approve',
`resource` VARCHAR(64) DEFAULT NULL COMMENT '所属资源, eg: order',
`action` VARCHAR(64) DEFAULT NULL COMMENT '操作, eg: approve',
`description` VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_perm_code` (`perm_code`),
INDEX `idx_resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';
-- 4. 用户-角色关联表
CREATE TABLE `user_role` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
`assigned_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '分配时间',
`assigner` VARCHAR(64) DEFAULT NULL COMMENT '分配人',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`),
INDEX `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
-- 5. 角色-权限关联表
CREATE TABLE `role_permission` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`role_id` BIGINT UNSIGNED NOT NULL COMMENT '角色ID',
`perm_id` BIGINT UNSIGNED NOT NULL COMMENT '权限ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_perm` (`role_id`, `perm_id`),
INDEX `idx_perm_id` (`perm_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';
需要注意:
- 关联表里我用了
UNIQUE KEY,防止同一条关联记录重复插入。 role_permission表里我没有加created_at,因为权限和角色的关系相对稳定,不需要追溯分配时间。但如果你的业务需要审计,加上也无妨。
4.4 核心查询 SQL:如何查用户权限?
表设计好了,最终要能查出「某个用户到底有哪些权限」。说白了,就是一个四表联查。
-- 查询用户 'zhangsan' 拥有的所有权限编码
SELECT DISTINCT p.perm_code
FROM users u
JOIN user_role ur ON u.id = ur.user_id
JOIN roles r ON ur.role_id = r.id
JOIN role_permission rp ON r.id = rp.role_id
JOIN permissions p ON rp.perm_id = p.id
WHERE u.username = 'zhangsan'
AND u.status = 1
AND r.is_system = 1; -- 或者根据业务过滤
一个小优化: 如果用户量和权限量非常大,我会建议在应用层做缓存。比如用户登录时,一次性把他的所有权限编码查出来,缓存到 Redis 里。避免每次请求都去联查这五张表,数据库扛不住的。
4.5 我在项目中踩过的一个坑
曾经,我在一个老项目里接手了一套权限系统。设计者把用户和角色的关联关系直接存在 users 表的一个 role_ids 字段里,用逗号分隔。当时觉得挺简单,后来要支持「一个用户有多个角色且不同角色有不同权限时」,查询逻辑变得极其复杂,而且索引完全用不上。
那次重构我花了整整两天时间,把数据迁移到标准的多对多关联表。所以我的经验是:一开始就老老实实用关联表,别走捷径。
4.6 扩展思考:双人审批场景下的表设计
我们这个课程的主题是 双人审批与 RBAC 融合。那么你可能会问:双人审批的权限表要不要单独设计?
我的建议是:不要单独建表。双人审批是一种流程模式,而不是一种新的权限类型。你可以在 permissions 表里增加一个字段,比如 approval_flow,标识这个权限是否需要双人审批,以及具体的审批角色。
举个例子:
ALTER TABLE `permissions`
ADD COLUMN `approval_flow` TINYINT UNSIGNED DEFAULT 0 COMMENT '审批模式: 0 单签, 1 双人审批, 2 会签',
ADD COLUMN `first_approver_role` BIGINT UNSIGNED DEFAULT NULL COMMENT '初审角色ID',
ADD COLUMN `second_approver_role` BIGINT UNSIGNED DEFAULT NULL COMMENT '复审角色ID';
这样,你既复用了现有的 RBAC 表结构,又为双人审批场景留出了扩展点。耦合度低,以后改起来也方便。