权限系统落地实战(下):RBAC五张表、按钮级权限、多租户隔离与PBAC策略引擎
上篇(权限模型全解析(上):别再写 if user.role == "admin" 了,ACL、RBAC四层与ABAC一次讲透)讲透了权限模型的理论:从 ACL 到 RBAC0/1/2/3,再到 ABAC 属性鉴权。但懂模型不等于能落地——表怎么设计?按钮怎么控?多租户怎么隔离?缓存怎么失效? 本文是下篇,全部用 Python(FastAPI 为主、Django 对比)给出工程级答案。
标签:RBAC、ABAC、ACL、权限模型、按钮级权限、数据级权限、多租户、PBAC
注:本文为下篇,建议先读上篇建立模型概念。
九、核心四:RBAC 工程实现
9.1 标准表结构
经典的 RBAC 五张表(+ 互斥表):
1 | -- 用户表 |
关键设计:
- 用户-角色、角色-权限都是多对多:一个用户可多角色,一个角色可多权限
- 角色
parent_id支持 RBAC1 继承:子角色自动继承父角色权限 - 权限
type区分菜单/按钮/接口:统一表管理,便于前端菜单和后端接口共用 tenant_id多租户隔离:每条数据带租户标识sys_role_conflict角色互斥:RBAC2 职责分离
为什么不用「用户直接挂权限点」的两表方案?回看上篇 5.2:500 人 × 50 个权限点 = 25000 行手工维护记录;五张表方案里,人员变动只动
sys_user_role一张表的几行。多出来的两张关联表,换来的是维护成本下降两个数量级。
9.2 权限编码规范
权限编码建议用 模块:资源:动作 三段式:
1 | user:view — 查看用户 |
规范的好处:
- 可读性强,一眼看出权限含义
- 便于通配匹配(如
user:*表示所有用户操作) - 便于按模块分配
1 | def match(code: str, pattern: str) -> bool: |
9.3 权限加载服务
1 | import json |
Django 对比:Django 自带
user.get_all_permissions()走的是Permission表 +_perm_cache(进程内缓存)。上表方案相当于把 Django 内置机制换成了「Redis 集中式缓存 + 自定义角色体系」,适合需要自定义角色/租户的场景。
9.4 权限校验:装饰器 + 依赖注入
对应 Java 的「注解 + AOP」,Python 的等价物是装饰器,FastAPI 再配上 Depends:
1 | from fastapi import Depends, HTTPException, status |
业务接口使用:
1 |
|
Django 对比:Django 内置等价物是
@permission_required("app.codename")或PermissionRequiredMixin,拿的是 Django 自带的权限点体系;要「模块:资源:动作」三段式编码就得自定义,写法与上面一致。
9.5 权限缓存失效策略
| 变更场景 | 失效操作 |
|---|---|
| 用户角色变更 | 清该用户缓存 |
| 角色权限变更 | 清所有持有该角色的用户缓存 |
| 用户登录/登出 | 重新加载 |
最易漏的:角色权限变更时,不能只清单个用户——要找到所有持有该角色的用户,全部失效。改了角色权限却没清缓存,导致权限变更不生效,这是常见 bug。
十、核心五:按钮级与接口级权限
前端「按钮可见性」和后端「接口可达性」是两层,缺一不可。
10.1 前端按钮级
前端根据用户权限编码,控制按钮显示:
1 | // 用户登录后,后端返回权限编码列表 |
10.2 后端接口级
1 |
|
关键:前端按钮隐藏只是体验优化(不让用户点无效按钮),后端接口必须独立校验。只隐藏前端按钮、不校验后端接口,攻击者直接 curl 构造请求就能绕过。
铁律:前端权限管「显示」,后端权限管「安全」,两者都要做,以后端为准。
10.3 菜单级权限
菜单权限是按钮权限的延伸——根据用户权限决定显示哪些菜单项。后端通常返回菜单树(已按权限过滤),前端直接渲染:
1 |
|
菜单树在 sys_permission 表里用 type=1 + parent_id 表达,过滤逻辑就是「节点及其子树中任一节点的权限编码命中 perms」。
十一、核心六:多租户权限隔离
SaaS 场景下,多租户权限隔离是硬需求——租户 A 的用户绝不能看到租户 B 的数据。
11.1 三种隔离方案
| 方案 | 做法 | 隔离强度 | 成本 | 适用 |
|---|---|---|---|---|
| 独立数据库 | 每租户一个库 | 最强 | 最高 | 大客户、强合规 |
| 共享库独立 schema | 每租户一个 schema | 中 | 中 | 中型租户 |
| 共享库共享表 + tenant_id | 所有数据加 tenant_id | 弱(靠代码保证) | 最低 | 小租户、量大 |
11.2 共享表方案的权限设计
最常用的是共享表 + tenant_id,权限模型要全程带 tenant_id:
1 | -- 所有表加 tenant_id |
用 SQLAlchemy 事件自动注入 tenant_id(对应 Java 的 MyBatis 拦截器):
1 | from contextvars import ContextVar |
tenant_ctx 由中间件在每个请求开始时从 JWT 中解析并写入,请求结束时清理:
1 |
|
权限规则:
- 用户-角色、角色-权限关联表都带
tenant_id - 角色、权限在租户内定义,租户间不通用
- 查询时全程带
tenant_id,拦截器统一注入
11.3 最易出的 bug
租户越权:查询时漏带 tenant_id 条件,导致租户 A 查到租户 B 的数据。这是 SaaS 最严重的安全事故。
防护:
- 用拦截器统一注入
tenant_id,业务代码不手写 - 关键接口加测试:用租户 A 的 token 访问租户 B 的资源 id,必须返回 404 而非 200
- 定期扫描代码,确保没有裸 SQL 漏带
tenant_id
11.4 超级管理员的特殊处理
超级管理员(平台运维)需要跨租户操作,不能用 tenant_id 隔离。通常做法:
- 超管走单独的「平台管理后台」,与租户后台物理分离
- 超管接口显式声明
@ignore_tenant(把tenant_ctx置None),跳过租户隔离 - 超管操作全程审计,避免滥用
十二、核心七:PBAC 策略引擎
复杂合规场景,RBAC + ABAC 仍不够,需要 PBAC 把规则集中到策略引擎。
12.1 为什么需要 PBAC
| 场景 | 痛点 |
|---|---|
| 权限规则极复杂 | 时间、地点、密级、审批流、上下文组合 |
| 规则散落各处 | 装饰器 + 代码 + 配置,难维护审计 |
| 规则频繁变更 | 改规则要重新发版 |
| 多系统权限统一 | 各系统权限规则不一致 |
12.2 PBAC 架构
graph LR
A["业务系统<br/>FastAPI / Django"] -->|"构造上下文<br/>subject/action/resource/env"| B["策略引擎<br/>OPA / Cedar"]
B -->|"allow / deny"| A
C[("策略库<br/>Rego / 策略文件")] --> B
D["审计日志"] --> B
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#f3e5f5
流程:
- 业务系统收到请求,构造「主体/客体/动作/环境」上下文
- 调用策略引擎(如 OPA)做决策
- 策略引擎按策略集计算,返回允许/拒绝
- 业务系统根据决策执行或拒绝
12.3 策略示例(OPA Rego 语法)
1 | # 允许查看本部门订单 |
Python 侧调用 OPA 只是一次 HTTP 请求:
1 | import httpx |
优势:规则与业务代码解耦,规则变更不重新发版,集中审计。
Python 生态里也有轻量替代:Casbin(
pip install casbin)用模型文件 + 策略文件表达 RBAC/ABAC,不需要独立部署 OPA 服务,中小场景更省事。上篇「对比专栏」已给出 Casbin 表达 ACL/RBAC/ABAC 的完整示例,这里补充它与 OPA 的工程选型对比:
维度 Casbin(pycasbin) OPA 形态 进程内库, pip install即用独立服务(Rego 语言) 延迟 函数调用级(微秒) HTTP 调用级(毫秒) 策略存放 policy 文件 / 数据库表 Rego 文件 / bundle 热更新 e.load_policy()或 watcherbundle 推送 跨系统统一 弱(各服务各嵌一份) 强(多系统问同一引擎) 学习成本 低(INI 模型 + CSV 策略) 中(需学 Rego) 经验法则:单体或少数几个服务 → Casbin;多系统共用一套策略、强审计合规 → OPA。也可以 Casbin 起步,规模化后把策略迁到 OPA——matcher 公式和 Rego 规则的语义是同构的,迁移主要是翻译而非重构。
12.4 何时上 PBAC
| 场景 | 用 PBAC? |
|---|---|
| 简单后台 RBAC | ❌ 用装饰器 + Depends 够 |
| 中型 + 数据级权限 | ❌ RBAC + ABAC 注解够 |
| 复杂合规、多系统统一 | ✅ 上 PBAC |
| 金融、政企、审计严 | ✅ 上 PBAC |
PBAC 不是替代 RBAC/ABAC,是用策略引擎统一承载这些模型。简单系统用 RBAC/ABAC 够了,复杂合规场景才上 PBAC。
十三、分场景讨论
13.1 简单后台
场景:用户少、角色固定(管理员/普通用户)。
方案:RBAC0 基础三层,甚至简化到「用户-角色」两表(角色对应固定权限点)。不必上继承、约束、ABAC。
13.2 中型企业系统
场景:部门多、角色多、有数据权限需求。
方案:RBAC3(继承 + 职责分离)+ 数据级 ABAC(@data_scope)。权限缓存 + 角色权限变更失效。
13.3 SaaS 多租户
场景:多租户,每租户独立角色体系。
方案:RBAC3 + 租户隔离(tenant_id 全程贯穿)+ 数据级 ABAC。租户内角色继承,租户间完全隔离。
13.4 复杂合规场景
场景:金融、政企,权限规则极复杂(时间、地点、密级、审批流)。
方案:PBAC 策略引擎——把权限规则抽到独立策略服务(如 OPA、Casbin),用规则引擎统一决策,业务系统调策略引擎。权限规则与业务代码彻底解耦。
13.5 开放平台
场景:对外开放 API,合作伙伴接入。
方案:RBAC(应用维度角色)+ OAuth2 scope(接口级授权)+ ABAC(数据级)。
十四、原理剖析:FastAPI 与 Django 的权限校验链
14.1 FastAPI 的依赖注入校验链
FastAPI 没有 Spring Security 那样的过滤器链,它的「链」是依赖注入的解析顺序:
1 | 请求 → ASGI Middleware(CORS / tenant_ctx 写入) |
关键组件:
| 组件 | 职责 |
|---|---|
Depends |
依赖声明,等价于 AOP 的「切点」 |
OAuth2PasswordBearer |
从 Authorization 头取 token |
current_user |
认证依赖,产出 User 对象 |
require_permission(...) |
授权依赖,校验权限点 |
HTTPException(401/403) |
认证失败 / 授权失败 |
依赖可以嵌套(require_permission 内部又 Depends(current_user)),框架会自动去重、按拓扑顺序执行——这就是 FastAPI 版的「权限校验链」。
14.2 Django 的权限校验链
Django 的权限校验走「中间件 + 后端(backend)」:
1 | 请求 → AuthenticationMiddleware(把 user 挂到 request) |
关键类:
| 组件 | 职责 |
|---|---|
AuthenticationMiddleware |
请求级注入 request.user |
ModelBackend |
默认权限后端,查 Permission 表 |
user.has_perm() |
授权入口 |
_perm_cache |
用户级权限缓存(进程内) |
两者对比:FastAPI 是「依赖在路由上显式声明,校验发生在进入函数前」;Django 是「Mixin/装饰器声明,中间件负责把用户准备好」。思想一致:认证产出 user,授权消费 user。
14.3 权限缓存的热点问题
权限缓存有个隐藏问题——角色权限变更时的「批量失效」:
1 | 角色权限变更 → 查所有持有该角色的用户 → 逐个清缓存 |
若一个角色被 10 万用户持有,变更一次要清 10 万个缓存 key,可能造成 Redis 压力峰值。
优化方案:
- 版本号机制:角色带版本号,缓存 key 带版本号(
perm:user:{uid}:{roleVersion}),角色变更时版本号 +1,老缓存自然过期,无需批量清 - 消息队列异步清:把批量清拆成消息,异步消费,避免同步阻塞
- 短 TTL:缓存 TTL 设短(如 5 分钟),容忍短暂不一致
1 | async def load_permissions(self, user_id: int) -> set[str]: |
版本号机制的精髓:不去删旧缓存,而是让旧缓存「作废」——key 变了,旧 key 无人读取,靠 TTL 自然蒸发。把 O(用户数) 的删除操作变成 O(1) 的自增。
十五、单元测试(pytest)
15.1 权限校验测试
1 | async def test_user_has_permission(permission_service): |
15.2 角色继承测试(RBAC1)
1 | async def test_role_inheritance(permission_service): |
15.3 职责分离测试(RBAC2)
1 | def test_static_sod_conflict(assign_role): |
15.4 数据级权限测试
1 | async def test_data_scope_self(client, user_a): |
15.5 多租户隔离测试
1 | async def test_tenant_isolation(client, tenant_a, tenant_b): |
十六、常见误区
16.1 误区:前端按钮隐藏就够了
误区:前端把无权按钮隐藏,用户点不到,安全了。
正解:前端隐藏只是体验优化,攻击者可绕过前端直接 curl 构造请求。后端接口必须独立校验权限。
16.2 误区:角色越细越好
误区:每个岗位一个角色,角色越细越灵活。
正解:角色太细会导致角色爆炸(几十上百个角色),管理反而困难。角色应对应「岗位或职责」,粒度适中,配合 ABAC 数据权限表达更细的需求。
16.3 误区:权限缓存永不失效
误区:权限缓存 TTL 设很长,减少查库。
正解:权限变更后缓存不失效,会导致权限变更不生效(撤销了权限但用户还能用)。必须有失效机制,角色权限变更要批量清相关用户缓存(或用版本号机制)。
16.4 误区:跨租户返回 403
误区:租户 A 用户访问租户 B 资源,返回 403(无权限)。
正解:应返回 404(不存在)。403 会泄露「资源存在但无权访问」,攻击者据此判断资源 id 是否有效;404 不泄露存在性。
16.5 误区:权限点硬编码在代码
误区:权限编码散落在各接口的装饰器里,没有统一管理。
正解:权限点应在 sys_permission 表统一管理,接口装饰器引用编码,便于审计和前端动态渲染。
十七、对比分析
17.1 权限模型对比
| 维度 | ACL | RBAC | ABAC | PBAC |
|---|---|---|---|---|
| 核心思想 | 资源列清单 | 用户-角色-权限 | 属性动态判断 | 策略引擎决策 |
| 适合 | 文件系统 | 业务系统 | 数据级权限 | 复杂合规 |
| 灵活性 | 低 | 中 | 高 | 最高 |
| 实现复杂度 | 低 | 中 | 中 | 高 |
| 维护成本 | 高(资源多) | 中 | 中 | 低(集中) |
| 性能 | 高 | 高 | 中(动态判断) | 低(调引擎) |
17.2 数据级权限方案对比
| 方案 | 实现 | 优缺点 |
|---|---|---|
| 手写 SQL 条件 | 每个查询手动加 where | 灵活但易漏,维护差 |
| 装饰器 + 事件注入 | @data_scope + SQLAlchemy 事件 |
统一,业务无感知 |
| 视图 | 数据库视图过滤 | 数据库层,难动态 |
| 行级安全(RLS) | PostgreSQL RLS | 数据库原生,但锁死数据库 |
17.3 多租户隔离方案对比
| 方案 | 隔离强度 | 成本 | 灵活性 |
|---|---|---|---|
| 独立库 | 最强 | 最高 | 低(迁移难) |
| 独立 schema | 中 | 中 | 中 |
| 共享表 + tenant_id | 弱(靠代码) | 最低 | 高 |
十八、最佳实践
18.1 模型选型
- 默认 RBAC0:80% 系统够用,简单成熟
- 角色继承用 RBAC1:避免重复定义,单继承够用
- 财务/审批用 RBAC2:职责分离防舞弊
- 数据级权限用 ABAC:装饰器 + 事件注入
- 复杂合规才上 PBAC:策略引擎集中决策
18.2 工程实现
- 统一表结构:五张标准表 + 互斥表
- 权限编码规范:
模块:资源:动作三段式 - 装饰器 + Depends 校验:业务无感知
- 缓存 + 失效机制:版本号机制优化批量失效
- 事件注入条件:数据权限、租户隔离自动注入
18.3 安全要点
- 后端必校验:不信任前端
- 跨租户返 404:不泄露资源存在
- 权限缓存要失效:变更即清,避免权限不生效
- 超管分离:平台管理与租户后台物理分离
- 审计日志:权限变更、敏感操作全程审计
十九、总结——核心要点速查表
| 要点 | 说明 |
|---|---|
| 权限三元组 | 主体-客体-动作 |
| RBAC0 基础三层 | 用户-角色-权限多对多 |
| RBAC1 角色继承 | parent_id 递归合并父角色权限 |
| RBAC2 职责分离 | 静态(分配时拒)/动态(执行时拒) |
| RBAC3 = RBAC1 + RBAC2 | 企业级标配 |
| ABAC 四属性 | 主体/客体/操作/环境 |
| 数据级权限 | 装饰器 + SQLAlchemy 事件注入 where |
| 前后端权限 | 前端管显示,后端管安全,以后端为准 |
| 多租户隔离 | tenant_id 全程贯穿,事件注入 |
| 跨租户返 404 | 不泄露资源存在 |
| 权限缓存 | 角色权限变更批量清,版本号机制优化 |
| PBAC 策略引擎 | 复杂合规场景,规则与代码解耦 |
二十、面试自测与答案
| # | 题目 | 对应章节 |
|---|---|---|
| Q1 | ACL 和 RBAC 的区别?为什么 RBAC 更适合企业系统? | 上篇 二、六 |
| Q2 | RBAC0/1/2/3 各是什么?角色继承解决什么问题? | 上篇 七 |
| Q3 | 什么是职责分离(SoD)?静态和动态职责分离的区别? | 上篇 7.3 |
| Q4 | RBAC 表达不了的什么场景要用 ABAC?举三个例子。 | 上篇 八 |
| Q5 | 数据级权限怎么实现?同一个接口不同人看不同数据的原理? | 上篇 8.4 |
| Q6 | 前端按钮级权限和后端接口级权限为什么要都做?只做前端行不行? | 十 |
| Q7 | 权限缓存怎么做?角色权限变更时如何失效?版本号机制是什么? | 9.5、14.3 |
| Q8 | 多租户 SaaS 的权限隔离有哪几种方案?共享表方案最易出什么 bug?跨租户应返回什么? | 十一 |
| Q9 | 为什么「制单」和「审核」角色不能同一人持有?这属于 RBAC 的哪一层? | 上篇 7.3 |
| Q10 | 复杂合规场景为什么要上 PBAC 策略引擎?与 RBAC/ABAC 的关系? | 十二 |
| Q11 | 权限编码为什么用「模块:资源:动作」三段式?有什么好处? | 9.2 |
| Q12 | 角色权限变更时批量清缓存有什么性能问题?如何优化? | 14.3 |
参考资料
- NIST RBAC 标准 (INCITS 359-2004) — RBAC 四层模型定义
- OWASP Access Control Cheat Sheet — 访问控制最佳实践
- FastAPI - OAuth2 with Password and Bearer — FastAPI 认证授权
- Django - Permissions — Django 内置权限
- Casbin — 多语言策略库(含 Python 绑定)
- OPA (Open Policy Agent) / Cedar / Drools — 策略引擎