3. RBAC 数据库表设计
3.1 设计原理
RBAC(基于角色的访问控制)的核心思想是将权限授予角色,再将角色授予用户,从而解耦用户与权限的直接绑定。这一设计需要五张数据库表:用户表、角色表、权限表以及两张关联表(用户‑角色关联表、角色‑权限关联表)。两张关联表分别处理“用户与角色”和“角色与权限”之间的多对多关系,保证数据的范式化和扩展性。
核心原则:用户只通过角色间接获得权限,禁止直接为用户分配权限。任何调整权限的需求仅需修改角色‑权限关联,无需遍历所有用户。
3.2 五张表结构详解
以下逐个列出每张表的字段设计,并以 MySQL 8.x 为参考。
3.2.1 用户表(users)
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| user_id | INT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | 用户唯一标识 |
| username | VARCHAR(50) | UNIQUE, NOT NULL | 登录用户名 |
| password_hash | VARCHAR(255) | NOT NULL | 加密后的密码 |
| VARCHAR(100) | — | 用户邮箱,可选 | |
| status | TINYINT | DEFAULT 1 | 1=启用,0=禁用 |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
3.2.2 角色表(roles)
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| role_id | INT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | 角色唯一标识 |
| role_name | VARCHAR(50) | UNIQUE, NOT NULL | 角色名称,如 admin, editor |
| description | VARCHAR(255) | — | 角色描述 |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
3.2.3 权限表(permissions)
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| permission_id | INT UNSIGNED | PRIMARY KEY, AUTO_INCREMENT | 权限唯一标识 |
| permission_name | VARCHAR(100) | UNIQUE, NOT NULL | 权限标识符,如 user:create |
| description | VARCHAR(255) | — | 权限说明 |
| created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 |
3.2.4 用户‑角色关联表(user_roles)
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| user_id | INT UNSIGNED | FOREIGN KEY → users(user_id) | 用户ID |
| role_id | INT UNSIGNED | FOREIGN KEY → roles(role_id) | 角色ID |
联合主键:PRIMARY KEY (user_id, role_id)。该表无独立业务主键,通过外键保证数据完整性。
3.2.5 角色‑权限关联表(role_permissions)
| 字段名 | 数据类型 | 约束 | 说明 |
|---|---|---|---|
| role_id | INT UNSIGNED | FOREIGN KEY → roles(role_id) | 角色ID |
| permission_id | INT UNSIGNED | FOREIGN KEY → permissions(permission_id) | 权限ID |
联合主键:PRIMARY KEY (role_id, permission_id)。
3.3 完整建表 SQL(MySQL 示例)
-- 用户表
CREATE TABLE users (
user_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(100),
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 角色表
CREATE TABLE roles (
role_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 权限表
CREATE TABLE permissions (
permission_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
permission_name VARCHAR(100) NOT NULL UNIQUE,
description VARCHAR(255),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 用户-角色关联表
CREATE TABLE user_roles (
user_id INT UNSIGNED NOT NULL,
role_id INT UNSIGNED NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 角色-权限关联表
CREATE TABLE role_permissions (
role_id INT UNSIGNED NOT NULL,
permission_id INT UNSIGNED NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(role_id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES permissions(permission_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.4 索引与性能优化
- 主键自动索引:每张表的主键已由 InnoDB 自动聚簇索引覆盖。
- 外键索引:
user_roles和role_permissions的外键列会因FOREIGN KEY自动创建索引,无需额外添加。 - 唯一索引:
username、role_name、permission_name已由UNIQUE约束建立索引。 - 查询建议:当需要按角色查询用户或按权限查询角色时,联合主键的顺序已支持从左到右的匹配,无需调整。
性能基线:单表百万级数据量下,通过主键或唯一索引检索耗时通常低于 1 ms;关联表因两列均为外键索引,多表 JOIN 也能保持在 10 ms 以内。
3.5 常见设计错误与避坑
- 为角色分配用户而非用户分配角色:应将角色作为中心,但实际业务中仍通过用户‑角色关联表实现双向关联,此处仅强调设计意图。
- 使用逗号分隔存储多角色:违反第一范式,导致无法使用索引,且更新时需要解析字符串。
- 缺少外键约束:应用程序逻辑可能产生孤立记录。务必定义
FOREIGN KEY且使用ON DELETE CASCADE(或RESTRICT视业务而定)。 - 权限粒度过细:每增加一个权限就新增一条记录,可能导致关联表数据膨胀。建议按模块+操作组合,如
user:create、user:delete。 - 忽略索引覆盖:若频繁按用户ID查询其所有角色,确保
user_roles的user_id列在索引最左侧(联合主键已满足)。
注意:生产环境中禁止在
VARCHAR 字段上使用 utf8mb4_general_ci 以外的校对规则默认即可;不要在关联表上额外添加自增主键,除非有其他复杂需求(如记录分配时间)。
3.6 设计验证与调试
建表后可通过以下 SQL 快速验证关联完整性:
-- 查询某个用户的所有角色(假设 user_id = 1)
SELECT r.role_id, r.role_name
FROM user_roles ur
JOIN roles r ON ur.role_id = r.role_id
WHERE ur.user_id = 1;
-- 查询某个角色的所有权限(假设 role_id = 2)
SELECT p.permission_id, p.permission_name
FROM role_permissions rp
JOIN permissions p ON rp.permission_id = p.permission_id
WHERE rp.role_id = 2;
-- 检查是否存在孤立记录(无对应父表)
SELECT * FROM user_roles WHERE user_id NOT IN (SELECT user_id FROM users);
若返回空结果,说明外键约束生效,数据一致。若有记录,应检查外键定义或手动清理。