一、为什么需要权限模型

任何多用户系统都要回答两个问题:

  1. 谁能访问(Authentication → 身份认证)
  2. 能做什么(Authorization → 授权)

权限模型回答的就是第二个问题。一套设计得当的权限模型能:

  • 让权限表达清晰、容易审计
  • 让新功能接入权限成本低
  • 让安全审计和合规检查有迹可循

二、ACL:访问控制列表

2.1 核心思想

每个资源绑定一份”谁能访问”的清单。

1
2
文件: 2026年财报.pdf
→ 允许: alice(读), bob(读写), carol(读)

2.2 数据结构

最简单的 ACL 就是「资源 × 用户 × 操作」三元的集合:

1
2
3
4
5
6
7
CREATE TABLE acl (
resource_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
permission VARCHAR(16) NOT NULL, -- read / write / delete
granted_at DATETIME NOT NULL,
PRIMARY KEY (resource_id, user_id, permission)
);

2.3 优缺点

优点

  • 直观,贴近现实世界的「给谁开门」心智
  • 细粒度,可以控制到具体资源实例
  • 实现简单,三张表就能跑

缺点

  • 用户和资源多了之后清单爆炸:1000 用户 × 10000 资源 × 3 操作 = 3000 万条记录
  • 修改权限要逐个资源改,不能批量
  • 审计时难以回答”这用户到底能干嘛”

2.4 典型场景

  • 操作系统文件权限(Linux chmod
  • 云存储 Bucket Policy(AWS S3)
  • 共享链接:临时给某用户开某个文件的访问权

ACL 在复杂业务里往往作为补充(比如给单个文档额外授权某人),而不是主力。

三、RBAC:基于角色的访问控制

3.1 核心思想

在「用户」和「权限」之间加一层角色作为中介。

1
2
3
4
5
用户 ──角色──> 权限

alice → admin → [用户管理, 内容管理, 数据导出]
bob → editor → [内容管理]
carol → viewer → [内容查看]

权限不再直接挂在用户身上,而是先挂在角色上,再把角色赋给用户。

3.2 五张基础表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
-- 用户
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(64) NOT NULL
);

-- 角色
CREATE TABLE roles (
id BIGINT PRIMARY KEY,
name VARCHAR(64) NOT NULL -- admin / editor / viewer
);

-- 权限
CREATE TABLE permissions (
id BIGINT PRIMARY KEY,
code VARCHAR(64) NOT NULL -- user:list / post:create / data:export
);

-- 用户-角色(多对多)
CREATE TABLE user_roles (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);

-- 角色-权限(多对多)
CREATE TABLE role_permissions (
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
PRIMARY KEY (role_id, permission_id)
);

权限判定伪代码

1
2
3
4
5
def has_permission(user, perm_code):
role_ids = get_role_ids(user.id)
perm_ids = [rp.permission_id for rp in role_permissions
if rp.role_id in role_ids]
return any(p.code == perm_code and p.id in perm_ids for p in permissions)

3.3 RBAC 三个变体

RBAC0 — 基础模型

就是上面那张图,用户-角色-权限三层多对多关系。95% 的后台系统停在 RBAC0 就够用

RBAC1 — 角色继承

角色之间有父子关系,子角色自动继承父角色的所有权限。

1
2
3
4
超级管理员
└─ 管理员
└─ 内容编辑
└─ 只读访客

数据上多加一张 role_parent(role_id, parent_id)

1
2
3
4
5
6
-- 内容编辑自动继承管理员的"用户列表"权限
INSERT INTO role_permissions (role_id, permission_id)
SELECT (SELECT id FROM roles WHERE name='内容编辑'),
permission_id
FROM role_permissions
WHERE role_id = (SELECT id FROM roles WHERE name='管理员');

Django 的 auth.Group 没有原生角色继承,但 django-guardian 或自研一层都能实现。

RBAC2 — 角色约束

在用户-角色关系上加限制:

约束类型 含义 业务例子
互斥角色 一个用户不能同时拥有 A 和 B 出纳 vs 会计(防一人既管钱又管账)
基数约束 一个角色最多 N 个人 「CEO」只能有 1 个
先决条件 想拿 B 必须先有 A 「副总经理」必须先是「部门经理」

数据库实现通常在 user_roles 上加触发器或应用层校验。

3.4 优缺点

优点

  • 用户-权限关系被拆成用户-角色 + 角色-权限,扩展性好
  • 调整一类人的权限 = 改一个角色,不用动每个用户
  • 角色是天然的”权限标签”,审计时一眼看清

缺点

  • 角色数量容易膨胀(几百个角色时管理成本变高)
  • 默认是粗粒度,不带”资源实例”维度(不能表达”只能改自己创建的订单”)

四、典型业务场景的权限设计

4.1 普通管理后台(OA / CMS)

最简模型:RBAC0,5 张表跑通。

菜单 / 按钮级权限

实际后台经常要控制”按钮显隐”,比如普通用户看不到”删除”按钮。常见做法:

1
2
3
4
5
-- 权限 code 用模块:动作命名
user:list -- 用户列表
user:create -- 新建用户
user:delete -- 删除用户
post:publish -- 发布文章

前端用 v-if="hasPerm('user:delete')" 控制按钮渲染,后端在中间件里再做一次校验(前端不可信)。

推荐实现

  • Python:Django + django.contrib.auth 内置 Permission + Group
  • Node:casl / accesscontrol

4.2 CRM 系统

CRM 比普通后台多一层:数据归属

「销售 A 只能看自己跟进的客户」——这是 RBAC 解决不了的(需要数据行级过滤)。

混合方案:RBAC(控制功能)+ 数据归属(控制行)

1
2
3
4
5
6
7
8
9
10
11
# Django ORM 配合 manager 的标准做法
class CustomerViewSet(viewsets.ModelViewSet):
serializer_class = CustomerSerializer

def get_queryset(self):
qs = Customer.objects.all()
user = self.request.user
# 销售只看自己的客户,经理看全部门
if user.has_perm('crm.view_all_customers'):
return qs
return qs.filter(owner=user)

关键点:

  • 功能权限用 RBAC(能不能看客户列表)
  • 数据权限走 ORM 层(能看到哪些客户)
  • 业界叫法不一:阿里叫「数据权限」,Salesforce 叫「Record-Level Security」

4.3 多租户 SaaS

多租户场景下,权限模型必须隔离两件事:

  1. 租户间:A 公司的管理员绝对看不到 B 公司的数据
  2. 租户内:A 公司内部还是要做 RBAC(普通员工 vs 管理员)

三种数据隔离方案对比

方案 共享库共享 schema 共享库独立 schema 独立库
隔离强度 弱(靠 tenant_id 过滤)
成本
迁移难度
适用规模 中小 SaaS(< 1000 租户) 中型 SaaS 大客户 / 金融

中小 SaaS 最常见:共享库 + tenant_id + 中间件过滤

1
2
3
4
5
6
7
8
9
10
11
12
# 中间件根据 JWT 自动注入 tenant_id 过滤
class TenantFilterMiddleware:
def __init__(self, get_response):
self.get_response = get_response

def __call__(self, request):
tenant_id = decode_jwt(request).get('tenant_id')
with tenant_context(tenant_id):
return self.get_response(request)

# Django + django-tenants 库提供现成方案
# 关键点:所有查询自动带 tenant_id 条件,绝不漏

超级管理员的”上帝视角”

平台方(运营团队)有时需要跨租户排查问题。设计模式:

  • 给平台账号一个特殊角色(如 super_admin),绕过租户过滤
  • 所有写操作记录审计日志(”谁在什么时间看了哪家公司的什么数据”)
  • 数据脱敏(手机号中间四位 **)

RBAC + 租户的完整结构

1
2
3
4
5
6
用户表
├─ tenant_id ← 属于哪个租户
└─ role_ids ← 租户内的角色

权限表
└─ 租户内通用,不区分租户

实现上推荐:

  • django-tenants:基于 PostgreSQL schema 隔离,Django 生态最成熟
  • Apache ShardingSphere:Java 生态的数据库代理层方案
  • Casbin multi-tenancy 模式:策略文件按租户分目录

五、常见踩坑

5.1 权限只在前端校验

前端按钮隐藏是给用户体验,真正的权限校验必须在后端。前端代码用户改一下 devtools 就能绕过。

5.2 权限模型一开始设计得太复杂

上来就搞 RBAC1 + RBAC2 + 数据权限 + 字段级权限,结果第一个版本上线就拖了三个月。

建议:MVP 阶段只做 RBAC0 + 菜单权限,后续按业务真实需求再扩展。

5.3 权限数据没做缓存

每次请求都查 5 张表,QPS 一高就崩。

1
2
3
4
5
6
7
8
9
# 用 Redis 缓存用户的权限码列表,TTL 5 分钟
def get_user_permissions(user_id):
key = f"perms:{user_id}"
cached = redis.get(key)
if cached:
return json.loads(cached)
perms = compute_permissions(user_id)
redis.setex(key, 300, json.dumps(perms))
return perms

角色调整时清缓存(或用消息队列广播失效)。

5.4 角色命名混乱

admin / super_admin / system_admin / manager —— 4 个月后没人记得清区别。

约定

  • 用「业务动作」而不是「职位」命名:order_managermanager 更准确
  • 维护一份权限字典表(permissions.code → 中文说明),所有模块共用

六、选型速查表

业务场景 推荐模型 推荐引擎
普通后台(OA/CMS) RBAC0 Django auth / Node casl
CRM RBAC + 数据权限行级过滤 Django + 自定义 manager
多租户 SaaS RBAC + tenant_id 隔离 django-tenants / ShardingSphere
文档协作 / 社交 ReBAC OpenFGA / Ory Keto
强合规要求 RBAC + ABAC 混合 Casbin / OPA
文件系统 / 简单 API ACL 直接用 ACL 表

七、参考资料

  • NIST RBAC 标准(INCITS 359-2012)
  • Django 官方文档 - Permissions and authorization
  • Apache Casbin 多模型支持
  • OpenFGA(Google Zanzibar 开源实现)

总结

  • ACL 适合”我给某个资源临时开个小门”的场景;
  • RBAC 是后台系统的默认选择,RBAC0 够用就别上 RBAC1/2;
  • CRM = RBAC + 数据行级过滤;
  • 多租户 = 租户隔离 + 租户内 RBAC,权限校验必须在后端,缓存要跟上。