Casbin:ACL、RBAC 与 ABAC 访问控制实践
本文重点放在“权限模型原理”和“模型之间的差异”,不再展开 PHP、Go 等具体代码实现。
模型配置和策略 CSV 仍然保留,因为它们是 Casbin 的授权 DSL,本身就是理解原理的核心。
1. 先分清:认证、授权、访问控制
| 概念 | 要回答的问题 | 典型实现 |
|---|---|---|
| 认证 Authentication | 你是谁? | 登录、Session、JWT、OAuth2 |
| 授权 Authorization | 你能不能做这件事? | Casbin、RBAC、ACL、ABAC |
| 访问控制 Access Control | 在什么条件下,谁可以对什么资源做什么操作? | subject + object + action + context -> allow/deny |
Casbin 主要解决授权,不负责替代登录系统。
典型链路:
用户登录
↓
认证系统验证身份,得到 user_id / claims
↓
业务接口准备 subject、object、action、domain、属性
↓
Casbin Enforcer 判断是否允许
↓
允许:继续业务
拒绝:返回 403访问控制的四个基本元素
request = (sub, obj, act[, dom, context])
sub Subject:谁,用户、服务账号、角色
obj Object:访问什么,接口、菜单、订单、文件
act Action:做什么,read、write、delete、approve
dom Domain:在哪个租户、组织、项目中
context:部门、资源 Owner、时间、IP、金额等属性最小判断通常是:
谁 sub 对什么 obj 做什么 act?多租户系统通常变成:
谁 sub 在哪个租户 dom 对什么 obj 做什么 act?2. 权限模型的演进关系
ACL
直接把权限给用户
RBAC
权限给角色,用户继承角色权限
RBAC with domains
在 RBAC 上增加作用域:租户、组织、项目
RBAC with pattern
在 RBAC 上增加资源模式匹配:/book/:id
RBAC with deny-override
在 RBAC 上增加显式拒绝和冲突裁决
ABAC
根据用户、资源、动作和环境的属性动态判断
ReBAC
根据用户和资源之间的关系判断它们不是互相替代关系,而是可以组合的权限维度。
3. ACL:直接给主体授权
ACL 的思路是:
每条权限直接写给某个主体:Alice 可以读取 data1。
策略示例:
p, alice, data1, read
p, bob, data2, write最小模型:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act适合:
- 小型系统
- 少量用户的资源授权
- 单独授权某个文件、某个订单给某个人
缺点:
- 用户多、资源多时,策略数量快速膨胀
- 批量修改权限困难
4. RBAC:权限给角色,用户继承角色
RBAC 的思路是:
权限给角色,用户通过角色获得权限。
策略示例:
p, admin, data1, read
p, admin, data1, write
p, reader, data1, read
g, alice, admin
g, bob, reader最小模型:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act关键变化只有一个:
ACL:r.sub == p.sub
RBAC:g(r.sub, p.sub)g 表示:
用户 → 角色
角色 → 角色角色可以继承:
g, super_admin, admin
g, admin, editor含义是:
super_admin 继承 admin 的权限
admin 继承 editor 的权限5. RBAC with domains:给 RBAC 增加作用域
要解决的问题
同一个用户在不同租户、组织、项目中,可能有不同角色:
alice 在 tenant_a 是 admin
alice 在 tenant_b 是 viewer如果继续使用普通 RBAC,就会写出:
tenant_a_admin
tenant_b_admin
tenant_a_viewer角色名会快速膨胀。
模型
[request_definition]
r = sub, dom, obj, act
[policy_definition]
p = sub, dom, obj, act
[role_definition]
g = _, _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub, r.dom) &&
r.dom == p.dom &&
r.obj == p.obj &&
r.act == p.act策略:
p, admin, tenant_a, report, read
p, admin, tenant_a, report, write
p, viewer, tenant_b, report, read
g, alice, admin, tenant_a
g, alice, viewer, tenant_b判断:
Enforce(alice, tenant_a, report, read)需要同时满足:
alice 在 tenant_a 中拥有 admin 角色
admin 在 tenant_a 中拥有 report:read 权限核心理解
用户 + domain → 角色
角色 + domain → 权限
请求必须携带 domaindomain 不一定叫 tenant,也可以是:
org_id
project_id
workspace_id
region_id
app_id常见错误
- 只给策略加 domain,不给用户角色加 domain
- Enforce 时漏传 domain
- 把租户编码进角色名
- 把租户编码进资源名
- 没有验证用户是否有权进入该 domain
和 ABAC 的区别
domain:结构性作用域,例如 tenant_a、project_1
ABAC:属性条件,例如部门、Owner、金额、时间domain 更适合表达“在哪个范围内”,ABAC 更适合表达“满足什么条件”。
6. RBAC with pattern:给 RBAC 增加资源模式匹配
要解决的问题
普通 RBAC 要写:
p, book_admin, /book/1, GET
p, book_admin, /book/2, GET
p, book_admin, /book/3, GETPattern RBAC 只写:
p, book_admin, /book/:id, GET它的本质仍然是 RBAC,只是把:
r.obj == p.obj换成:
keyMatch2(r.obj, p.obj)模型
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) &&
keyMatch2(r.obj, p.obj) &&
r.act == p.act策略:
p, book_admin, /book/:id, GET
g, alice, book_admin请求:
alice, /book/1, GET普通 RBAC 和 Pattern RBAC 的区别
| 维度 | 普通 RBAC | RBAC with pattern |
|---|---|---|
| 用户和角色 | g 关系 | 一样 |
| 角色和权限 | p 关系 | 一样 |
| 对象匹配 | 精确相等 | 模式匹配 |
| 策略数量 | 通常随资源数量增长 | 按资源类型写一条 |
| 典型对象 | order:123 | /order/:id |
| 典型动作 | read | GET |
keyMatch2 和 regexMatch 的区别
| 函数 | 用途 |
|---|---|
keyMatch2 | 匹配 REST 路径,例如 /book/:id |
keyMatch | 简单路径通配符 |
regexMatch | 使用正则表达式 |
r.obj == p.obj | 完全相等匹配 |
r.act == p.act | HTTP 方法完全相等匹配 |
regexMatch 默认是部分匹配,不是完整匹配。 如果使用正则,通常要写成:
^GET$关于 g 中的模式
下面这条规则:
g, /book/:id, pen_admin默认含义只是:
字符串 /book/:id 继承 pen_admin普通 g 默认是精确匹配。 要让 g 内部的 /book/:id 变成模式,需要在宿主程序中注册匹配函数,例如为 g 注册 KeyMatch2。
也就是说:
模型文本本身不会自动把 :id 变成通配符Pattern 的风险
p, viewer, /book/*, GET可能覆盖你不想开放的路径。
p, viewer, /book/:id, GET通常表示“可以查看所有书”,不是“只能查看自己的书”。
所以 Pattern 只解决:
能访问哪一类资源不能解决:
只能访问属于自己的那一本7. deny-override:显式拒绝优先
要解决的问题
普通 allow-list 只能表达:
命中 allow 就通过
没有命中就拒绝它无法优雅表达:
允许管理员删除普通数据
但是禁止删除系统内置数据模型
策略定义增加 eft:
[policy_definition]
p = sub, obj, act, eft效果:
[policy_effect]
e = some(where (p.eft == allow)) &&
!some(where (p.eft == deny))含义:
只要命中任意 deny,最终就是 deny
没有 deny,并且有 allow,才 allow策略:
p, book_admin, /book/:id, GET, allow
p, book_admin, /book/secret, GET, deny请求:
alice, /book/1, GET
=> allow请求:
alice, /book/secret, GET
=> deny适用场景
- 黑名单
- 禁用数据
- 敏感操作禁止
- 系统内置数据保护
- 临时封禁
核心理解
deny-override 不是新的角色模型
它改变的是 policy_effect 的裁决方式Priority
Priority 解决的是:
哪条规则优先级更高典型策略定义:
p = sub, obj, act, eft, priority它适合:
- 具体规则覆盖通用规则
- 特殊客户例外
- 临时授权
- 多套策略叠加
Deny-override 和 priority 经常一起出现,但解决的问题不同:
deny-override:有没有 deny
priority:哪条规则优先8. ABAC:根据属性动态判断
ABAC 的思路是:
不只看用户是谁或属于什么角色,还看用户、资源、动作和环境的属性。
例如:
用户部门 = 资源部门
并且 用户职级 >= 资源要求职级
并且 动作 = read
=> allow属性可以来自:
| 类型 | 属性示例 |
|---|---|
| Subject | 用户部门、职级、岗位、账号状态 |
| Object | 资源所属部门、Owner、敏感级别、金额 |
| Action | read、write、approve、download |
| Environment | 当前时间、IP、设备、地理位置、来源系统 |
ABAC 常见 matcher
资源 Owner:
m = r.act == "read" && r.sub == r.obj.Owner部门和职级:
m = r.act == "read" &&
r.sub.Department == r.obj.Department &&
r.sub.Level >= r.obj.RequiredLevelABAC 的关键原则
- 属性必须来自可信来源
- 不能直接相信客户端传来的部门、职级、Owner
- 属性为空、类型错误、查询失败时,必须 fail-closed
- 属性越复杂,越需要测试和审计
- ABAC 可以单独使用,也可以和 RBAC 组合
RBAC + ABAC 混合
RBAC:判断用户是否具备审核员能力
ABAC:判断用户是否只能审核本部门且金额 <= 10000 的订单这类组合在真实系统中很常见。
ABAC 和其他模型的区别
| 模型 | 判断依据 |
|---|---|
| ACL | 用户和资源的直接关系 |
| RBAC | 用户所属角色 |
| RBAC with domains | 用户、角色、作用域 |
| ABAC | 用户、资源、动作、环境属性 |
| ReBAC | 用户和资源之间的关系 |
domain 是结构性作用域,ABAC 是动态属性条件。
9. ReBAC:根据关系判断
ReBAC 的典型例子:
Alice 是文档 doc1 的 owner
Bob 是文件夹 folder1 的 editor
doc1 属于 folder1
所以 Bob 可以编辑 doc1适合:
- Google Drive 式共享
- Notion 式协作
- 云盘、素材库、知识库
- 组织、团队、项目、资源的多级关系
ReBAC 通常使用:
g
g2
g3等关系定义,把“用户—资源—资源组”的关系表达出来。
当业务开始出现“共享、协作者、继承、父子资源”时,再学习它。
10. 如何快速读懂任何 Casbin 模型
看到任何 Casbin 模型,先问五个问题:
r定义了什么请求参数?p定义了什么策略字段?- 是否存在
g、g2、g3?它们表达什么关系? matchers用了哪些匹配函数?policy_effect怎么处理 allow、deny、priority?
对应关系:
| 变化 | 对应模型 |
|---|---|
g(r.sub, p.sub) | RBAC |
r = sub, dom, obj, act | RBAC with domains |
g(r.sub, p.sub, r.dom) | 多租户 RBAC |
keyMatch2(r.obj, p.obj) | RBAC with pattern |
p.eft == deny | deny-override |
priority(p.eft) | priority |
r.sub.Department == r.obj.Department | ABAC |
r.sub == r.obj.Owner | ABAC |
g(r.sub, r.obj, p.role) | ReBAC |
核心理解:
Casbin 不是一组写死的权限模型
而是一个由 request、policy、role、matcher、effect 组成的授权计算框架11. 模型选择决策树
只有少量用户和资源的直接授权?
└─ ACL
用户通过岗位/角色获得固定权限?
└─ RBAC
权限依赖租户、组织、项目等作用域?
└─ RBAC with domains
资源是 /book/:id、/order/:id 这类路径?
└─ RBAC with pattern
需要显式拒绝、黑名单或规则优先级?
└─ deny-override / priority
权限依赖部门、Owner、金额、时间、IP 等属性?
└─ ABAC
权限依赖共享、协作者、资源关系?
└─ ReBAC
既有角色能力,又有资源属性限制?
└─ RBAC + ABAC
多个租户,用户在不同租户角色不同?
└─ RBAC with domains
需要少量特殊例外?
└─ 主模型 + 有边界的 ACL 例外不要为了“看起来高级”直接使用复杂 ABAC。模型越复杂,越需要:
- 明确字段契约
- 规则单元测试
- 权限变更审计
- 失败时的可诊断日志
- 多租户隔离测试
12. 生产环境常见问题
1. 把认证当成授权
JWT 有效只说明令牌可能有效,不代表当前用户可以操作当前订单。
2. 只在前端隐藏按钮
前端隐藏按钮属于体验优化,不是安全控制。真正的授权检查必须在后端业务边界执行。
3. RBAC 角色越来越多
如果出现:
finance_reader_region_a_amount_1000
finance_reader_region_b_amount_5000
finance_reader_region_c_amount_10000通常说明把动态属性硬编码进了角色。可以考虑:
RBAC:finance_reader
ABAC:区域匹配 + 金额阈值判断4. ABAC 属性来源不可信
不要直接相信客户端传来的:
{
"department": "finance",
"level": 99
}用户属性应来自认证服务、数据库或可信服务,并在进入授权边界前完成校验。
5. 多租户 domain 漏传或串租户
所有租户隔离资源都要明确:
domain 从哪里来?
谁负责校验用户属于该 domain?
策略是否包含 domain?
Enforce 参数顺序是否固定?6. 多实例策略不同步
如果策略存储在数据库,但多个应用实例已经把策略加载到内存,那么一个实例修改策略后,其他实例可能仍然使用旧策略。
生产环境需要设计:
- Adapter:策略如何持久化
- Watcher:策略变更如何通知其他实例
- Reload:收到通知后如何重新加载
- Cache:缓存何时失效
- Audit:谁在什么时候修改了什么权限
7. Pattern 匹配范围过宽
路径匹配规则要有单元测试,尤其关注:
- 通配符
- 尾斜杠
- URL 编码
- 大小写
- 参数段
- 父子路径
8. 权限校验对象不稳定
不要用展示名称作为 object:
“财务报表”应使用稳定标识:
report:finance:monthly
order:12313. 最小测试矩阵
至少覆盖以下情况:
| 场景 | 预期 |
|---|---|
| 直接命中允许策略 | allow |
| 完全没有策略 | deny |
| 用户通过角色获得权限 | allow |
| 角色继承获得权限 | allow |
| 角色存在但资源不匹配 | deny |
| action 不匹配 | deny |
| domain 不匹配 | deny |
| Pattern 匹配范围错误 | deny |
| ABAC 部门不匹配 | deny |
| ABAC 等级不足 | deny |
| 显式 deny 与 allow 同时命中 | 按 policy_effect 规则判断 |
| Enforce 返回错误 | fail-closed |
| 策略变更后其他实例 | 按同步方案验证 |
| 未登录用户直接调用 API | 401 |
| 已登录但无权限 | 403 |
推荐把权限测试写成表格驱动测试:
输入 subject / domain / object / action / attributes
预期 allowed / error这样比只测试某一个管理员账号更容易发现边界问题。
15. 一句话记忆
ACL:权限直接给人
RBAC:权限给角色,人继承角色
RBAC with domains:权限还要限定在哪个租户/组织/项目
RBAC with pattern:角色权限可以覆盖一类资源路径
deny-override:显式拒绝优先
ABAC:根据属性动态判断
ReBAC:根据关系判断Casbin 的核心不是记住模型名字,而是理解:
Request 定义输入
Policy 定义规则
Role 定义关系
Matcher 定义计算
Effect 定义裁决