Files
autumn-recruitment/07.模式/auth/RBAC 权限模型.md
T

262 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [pattern/auth, rbac, role-inheritance, separation-of-duty, access-control]
create time: 2026-08-08 15:30
update time: 2026-08-08 15:30
---
# RBAC 权限模型
## 概述
RBAC(Role-Based Access Control)是一种基于角色分配访问权限的模型,是目前企业级应用中最主流的权限管理方案。它的核心思想是**将用户与角色关联,而非将用户与权限直接绑定**——这层间接大大降低了权限管理的复杂度。
## 五张表设计
### 表结构定义
```sql
-- 1. 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL UNIQUE,
password_hash VARCHAR(128) NOT NULL,
email VARCHAR(128),
status TINYINT DEFAULT 1 COMMENT '1-active 0-disabled',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 2. 角色表
CREATE TABLE roles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64) NOT NULL UNIQUE,
description VARCHAR(256),
is_super TINYINT DEFAULT 0 COMMENT '是否超级管理员',
parent_id BIGINT DEFAULT NULL COMMENT '父角色ID(用于继承)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 3. 权限表
CREATE TABLE permissions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(128) NOT NULL UNIQUE COMMENT '权限标识如 user:create',
resource VARCHAR(64) NOT NULL COMMENT '资源类型: user/order/bucket',
action VARCHAR(64) NOT NULL COMMENT '操作: create/read/update/delete',
description VARCHAR(256)
);
-- 4. 用户-角色(多对多)
CREATE TABLE user_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
-- 5. 角色-权限(多对多)
CREATE TABLE role_permissions (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id),
FOREIGN KEY (role_id) REFERENCES roles(id),
FOREIGN KEY (permission_id) REFERENCES permissions(id)
);
```
### ER 关系图
```mermaid
erDiagram
users ||--o{ user_roles : "belongs to"
roles ||--o{ user_roles : "assigned via"
roles ||--o{ role_permissions : "has"
permissions ||--o{ role_permissions : "granted through"
users {
BIGINT id PK
varchar username
varchar password_hash
tinyint status
}
roles {
BIGINT id PK
varchar name
varchar description
tinyint is_super
BIGINT parent_id FK
}
permissions {
BIGINT id PK
varchar code UK
varchar resource
varchar action
}
user_roles {
BIGINT user_id PK
BIGINT role_id PK
}
role_permissions {
BIGINT role_id PK
BIGINT permission_id PK
}
```
## 核心原理
### 用户 → 角色 → 权限 的两层映射
传统 ACL(Access Control List)直接将用户和权限绑定:
```
Alice → read_order, write_order, delete_user
Bob → read_order
Carol → read_order, write_bucket
```
问题在于当需要批量授权时(比如给所有财务加 20 个权限),必须逐一修改每个用户的权限列表。
RBAC 引入角色层后变成:
```
角色定义:
Finance → 20 个权限(包括 read_order, export_report...)
Operator → 10 个权限(read_order, write_bucket...)
用户分配:
Alice → Finance
Bob → Operator
Carol → Operator
```
批量调整只需改角色的权限映射,不影响用户。
### 超级管理员与普通角色的继承
超级管理员通常拥有所有权限。但直接给 super_admin 角色分配几百条权限记录既繁琐也不优雅。更合理的做法是**继承机制**:
```sql
-- 在 roles 表中用 parent_id 表示层级
INSERT INTO roles (name, is_super, parent_id) VALUES ('super_admin', 1, NULL);
INSERT INTO roles (name, is_super, parent_id) VALUES ('admin', 0, (SELECT id FROM roles WHERE name='super_admin'));
INSERT INTO roles (name, is_super, parent_id) VALUES ('finance', 0, (SELECT id FROM roles WHERE name='admin'));
```
查询某用户全部权限时递归展开:
```go
func (m *AuthManager) GetUserPermissions(ctx context.Context, userID int64) ([]string, error) {
// 1. 查用户所有角色
roles, _ := m.db.QueryContext(ctx, `
SELECT r.id, r.is_super, r.parent_id FROM user_roles ur
JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ?`, userID)
var allCodes []string
for roles.Next() {
var role Role
roles.Scan(&role.ID, &role.IsSuper, &role.ParentID)
if role.IsSuper {
return []string{"*"}, nil
}
perms, _ := m.getRolePermissionCodes(ctx, role.ID)
allCodes = append(allCodes, perms...)
}
return deduplicate(allCodes), nil
}
```
### 互斥职责分离(Separation of Duty)
某些场景下同一人不能同时拥有冲突的角色,例如:
- **申请人与审批人不可同一人** — 你不能批准自己的报销单
- **出纳与会计不可兼任** — 财务内控基本要求
实现方式是在角色创建或用户分配时做冲突检测:
```sql
-- 假设 conflict_roles 表定义互斥关系
CREATE TABLE conflict_roles (
role_a BIGINT NOT NULL,
role_b BIGINT NOT NULL,
PRIMARY KEY (role_a, role_b)
);
-- 分配角色前检查
SELECT COUNT(*) FROM conflict_roles cr
JOIN user_roles ur ON ur.role_id IN (cr.role_a, cr.role_b)
WHERE ur.user_id = ? AND cr.role_a = ? AND cr.role_b = ?
```
> [!WARNING]
> 互斥约束应该在**分配角色层**校验,而不是每次鉴权时检查。前者是策略问题,后者是执行问题——在入口处挡住比到处拦击效率高得多。
### 动态权限加载流程
```mermaid
sequenceDiagram
participant User as 用户
participant API as 认证服务
participant DB as 数据库
participant Redis as 缓存
User->>API: 登录
API->>DB: 查询用户所有角色
DB-->>API: 返回角色列表
API->>DB: 查询各角色的权限码
DB-->>API: 返回权限码列表
API->>Redis: 写入用户权限缓存 (TTL=2h)
Redis-->>API: OK
API-->>User: 登录成功 + Token
```
> [!TIP]
> 登录时将完整权限树缓存在 Redis 中,鉴权接口只需 O(1) 读取缓存。权限变更时主动删除对应用户的缓存条目,实现最终一致。不要每次都查库。
## 代码示例:权限中间件(Go)
```go
type ContextKey string
const PermissionKey ContextKey = "permissions"
func AuthMiddleware(h Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := extractToken(r)
claims, _ := jwt.Parse(token)
userID := claims.UserID
// 从 Redis 获取权限
perms, err := cache.GetPermissions(r.Context(), userID)
if err != nil {
perms = loadFromDB(userID)
cache.Set(userID, perms, 2*time.Hour)
}
ctx := context.WithValue(r.Context(), PermissionKey, perms)
h.ServeHTTP(w, r.WithContext(ctx))
}
}
// 检查某个权限
func HasPermission(ctx context.Context, required string) bool {
perms := ctx.Value(PermissionKey).([]string)
for _, p := range perms {
if p == "*" || p == required {
return true
}
}
return false
}
```
## 实践场景
| 场景 | 选型建议 |
|------|---------|
| 小型系统(用户 < 100) | 简单角色 + 硬编码判断即可,不必上完整 RBAC |
| 中型业务系统 | 标准五表 RBAC + Redis 缓存 |
| 平台型 SaaS | RBAC + 租户隔离(每租户可自定义角色) |
| 强合规要求(金融/医疗) | RBAC + SoD 互斥约束 + 完整审计日志 |
## 关联笔记
- [[OAuth2 与 JWT]]