Skip to content

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 主要解决授权,不负责替代登录系统。

典型链路:

text
用户登录
  ↓
认证系统验证身份,得到 user_id / claims
  ↓
业务接口准备 subject、object、action、domain、属性
  ↓
Casbin Enforcer 判断是否允许
  ↓
允许:继续业务
拒绝:返回 403

访问控制的四个基本元素 ​

text
request = (sub, obj, act[, dom, context])

sub   Subject:谁,用户、服务账号、角色
obj   Object:访问什么,接口、菜单、订单、文件
act   Action:做什么,read、write、delete、approve
dom   Domain:在哪个租户、组织、项目中
context:部门、资源 Owner、时间、IP、金额等属性

最小判断通常是:

text
谁 sub 对什么 obj 做什么 act?

多租户系统通常变成:

text
谁 sub 在哪个租户 dom 对什么 obj 做什么 act?

2. 权限模型的演进关系 ​

text
ACL
  直接把权限给用户

RBAC
  权限给角色,用户继承角色权限

RBAC with domains
  在 RBAC 上增加作用域:租户、组织、项目

RBAC with pattern
  在 RBAC 上增加资源模式匹配:/book/:id

RBAC with deny-override
  在 RBAC 上增加显式拒绝和冲突裁决

ABAC
  根据用户、资源、动作和环境的属性动态判断

ReBAC
  根据用户和资源之间的关系判断

它们不是互相替代关系,而是可以组合的权限维度。


3. ACL:直接给主体授权 ​

ACL 的思路是:

每条权限直接写给某个主体:Alice 可以读取 data1。

策略示例:

csv
p, alice, data1, read
p, bob, data2, write

最小模型:

ini
[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 的思路是:

权限给角色,用户通过角色获得权限。

策略示例:

csv
p, admin, data1, read
p, admin, data1, write
p, reader, data1, read

g, alice, admin
g, bob, reader

最小模型:

ini
[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

关键变化只有一个:

text
ACL:r.sub == p.sub
RBAC:g(r.sub, p.sub)

g 表示:

text
用户 → 角色
角色 → 角色

角色可以继承:

csv
g, super_admin, admin
g, admin, editor

含义是:

text
super_admin 继承 admin 的权限
admin 继承 editor 的权限

5. RBAC with domains:给 RBAC 增加作用域 ​

要解决的问题 ​

同一个用户在不同租户、组织、项目中,可能有不同角色:

text
alice 在 tenant_a 是 admin
alice 在 tenant_b 是 viewer

如果继续使用普通 RBAC,就会写出:

text
tenant_a_admin
tenant_b_admin
tenant_a_viewer

角色名会快速膨胀。

模型 ​

ini
[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

策略:

csv
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

判断:

text
Enforce(alice, tenant_a, report, read)

需要同时满足:

text
alice 在 tenant_a 中拥有 admin 角色
admin 在 tenant_a 中拥有 report:read 权限

核心理解 ​

text
用户 + domain → 角色
角色 + domain → 权限
请求必须携带 domain

domain 不一定叫 tenant,也可以是:

text
org_id
project_id
workspace_id
region_id
app_id

常见错误 ​

  • 只给策略加 domain,不给用户角色加 domain
  • Enforce 时漏传 domain
  • 把租户编码进角色名
  • 把租户编码进资源名
  • 没有验证用户是否有权进入该 domain

和 ABAC 的区别 ​

text
domain:结构性作用域,例如 tenant_a、project_1
ABAC:属性条件,例如部门、Owner、金额、时间

domain 更适合表达“在哪个范围内”,ABAC 更适合表达“满足什么条件”。


6. RBAC with pattern:给 RBAC 增加资源模式匹配 ​

要解决的问题 ​

普通 RBAC 要写:

csv
p, book_admin, /book/1, GET
p, book_admin, /book/2, GET
p, book_admin, /book/3, GET

Pattern RBAC 只写:

csv
p, book_admin, /book/:id, GET

它的本质仍然是 RBAC,只是把:

text
r.obj == p.obj

换成:

text
keyMatch2(r.obj, p.obj)

模型 ​

ini
[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

策略:

csv
p, book_admin, /book/:id, GET
g, alice, book_admin

请求:

text
alice, /book/1, GET

普通 RBAC 和 Pattern RBAC 的区别 ​

维度普通 RBACRBAC with pattern
用户和角色g 关系一样
角色和权限p 关系一样
对象匹配精确相等模式匹配
策略数量通常随资源数量增长按资源类型写一条
典型对象order:123/order/:id
典型动作readGET

keyMatch2 和 regexMatch 的区别 ​

函数用途
keyMatch2匹配 REST 路径,例如 /book/:id
keyMatch简单路径通配符
regexMatch使用正则表达式
r.obj == p.obj完全相等匹配
r.act == p.actHTTP 方法完全相等匹配

regexMatch 默认是部分匹配,不是完整匹配。 如果使用正则,通常要写成:

text
^GET$

关于 g 中的模式 ​

下面这条规则:

csv
g, /book/:id, pen_admin

默认含义只是:

text
字符串 /book/:id 继承 pen_admin

普通 g 默认是精确匹配。 要让 g 内部的 /book/:id 变成模式,需要在宿主程序中注册匹配函数,例如为 g 注册 KeyMatch2。

也就是说:

text
模型文本本身不会自动把 :id 变成通配符

Pattern 的风险 ​

csv
p, viewer, /book/*, GET

可能覆盖你不想开放的路径。

csv
p, viewer, /book/:id, GET

通常表示“可以查看所有书”,不是“只能查看自己的书”。

所以 Pattern 只解决:

text
能访问哪一类资源

不能解决:

text
只能访问属于自己的那一本

7. deny-override:显式拒绝优先 ​

要解决的问题 ​

普通 allow-list 只能表达:

text
命中 allow 就通过
没有命中就拒绝

它无法优雅表达:

text
允许管理员删除普通数据
但是禁止删除系统内置数据

模型 ​

策略定义增加 eft:

ini
[policy_definition]
p = sub, obj, act, eft

效果:

ini
[policy_effect]
e = some(where (p.eft == allow)) &&
    !some(where (p.eft == deny))

含义:

text
只要命中任意 deny,最终就是 deny
没有 deny,并且有 allow,才 allow

策略:

csv
p, book_admin, /book/:id, GET, allow
p, book_admin, /book/secret, GET, deny

请求:

text
alice, /book/1, GET
=> allow

请求:

text
alice, /book/secret, GET
=> deny

适用场景 ​

  • 黑名单
  • 禁用数据
  • 敏感操作禁止
  • 系统内置数据保护
  • 临时封禁

核心理解 ​

text
deny-override 不是新的角色模型
它改变的是 policy_effect 的裁决方式

Priority ​

Priority 解决的是:

text
哪条规则优先级更高

典型策略定义:

ini
p = sub, obj, act, eft, priority

它适合:

  • 具体规则覆盖通用规则
  • 特殊客户例外
  • 临时授权
  • 多套策略叠加

Deny-override 和 priority 经常一起出现,但解决的问题不同:

text
deny-override:有没有 deny
priority:哪条规则优先

8. ABAC:根据属性动态判断 ​

ABAC 的思路是:

不只看用户是谁或属于什么角色,还看用户、资源、动作和环境的属性。

例如:

text
用户部门 = 资源部门
并且 用户职级 >= 资源要求职级
并且 动作 = read
=> allow

属性可以来自:

类型属性示例
Subject用户部门、职级、岗位、账号状态
Object资源所属部门、Owner、敏感级别、金额
Actionread、write、approve、download
Environment当前时间、IP、设备、地理位置、来源系统

ABAC 常见 matcher ​

资源 Owner:

ini
m = r.act == "read" && r.sub == r.obj.Owner

部门和职级:

ini
m = r.act == "read" &&
    r.sub.Department == r.obj.Department &&
    r.sub.Level >= r.obj.RequiredLevel

ABAC 的关键原则 ​

  1. 属性必须来自可信来源
  2. 不能直接相信客户端传来的部门、职级、Owner
  3. 属性为空、类型错误、查询失败时,必须 fail-closed
  4. 属性越复杂,越需要测试和审计
  5. ABAC 可以单独使用,也可以和 RBAC 组合

RBAC + ABAC 混合 ​

text
RBAC:判断用户是否具备审核员能力
ABAC:判断用户是否只能审核本部门且金额 <= 10000 的订单

这类组合在真实系统中很常见。

ABAC 和其他模型的区别 ​

模型判断依据
ACL用户和资源的直接关系
RBAC用户所属角色
RBAC with domains用户、角色、作用域
ABAC用户、资源、动作、环境属性
ReBAC用户和资源之间的关系

domain 是结构性作用域,ABAC 是动态属性条件。


9. ReBAC:根据关系判断 ​

ReBAC 的典型例子:

text
Alice 是文档 doc1 的 owner
Bob 是文件夹 folder1 的 editor
doc1 属于 folder1
所以 Bob 可以编辑 doc1

适合:

  • Google Drive 式共享
  • Notion 式协作
  • 云盘、素材库、知识库
  • 组织、团队、项目、资源的多级关系

ReBAC 通常使用:

text
g
g2
g3

等关系定义,把“用户—资源—资源组”的关系表达出来。

当业务开始出现“共享、协作者、继承、父子资源”时,再学习它。


10. 如何快速读懂任何 Casbin 模型 ​

看到任何 Casbin 模型,先问五个问题:

  1. r 定义了什么请求参数?
  2. p 定义了什么策略字段?
  3. 是否存在 g、g2、g3?它们表达什么关系?
  4. matchers 用了哪些匹配函数?
  5. policy_effect 怎么处理 allow、deny、priority?

对应关系:

变化对应模型
g(r.sub, p.sub)RBAC
r = sub, dom, obj, actRBAC with domains
g(r.sub, p.sub, r.dom)多租户 RBAC
keyMatch2(r.obj, p.obj)RBAC with pattern
p.eft == denydeny-override
priority(p.eft)priority
r.sub.Department == r.obj.DepartmentABAC
r.sub == r.obj.OwnerABAC
g(r.sub, r.obj, p.role)ReBAC

核心理解:

text
Casbin 不是一组写死的权限模型
而是一个由 request、policy、role、matcher、effect 组成的授权计算框架

11. 模型选择决策树 ​

text
只有少量用户和资源的直接授权?
  └─ 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 角色越来越多 ​

如果出现:

text
finance_reader_region_a_amount_1000
finance_reader_region_b_amount_5000
finance_reader_region_c_amount_10000

通常说明把动态属性硬编码进了角色。可以考虑:

text
RBAC:finance_reader
ABAC:区域匹配 + 金额阈值判断

4. ABAC 属性来源不可信 ​

不要直接相信客户端传来的:

json
{
  "department": "finance",
  "level": 99
}

用户属性应来自认证服务、数据库或可信服务,并在进入授权边界前完成校验。

5. 多租户 domain 漏传或串租户 ​

所有租户隔离资源都要明确:

text
domain 从哪里来?
谁负责校验用户属于该 domain?
策略是否包含 domain?
Enforce 参数顺序是否固定?

6. 多实例策略不同步 ​

如果策略存储在数据库,但多个应用实例已经把策略加载到内存,那么一个实例修改策略后,其他实例可能仍然使用旧策略。

生产环境需要设计:

  • Adapter:策略如何持久化
  • Watcher:策略变更如何通知其他实例
  • Reload:收到通知后如何重新加载
  • Cache:缓存何时失效
  • Audit:谁在什么时候修改了什么权限

7. Pattern 匹配范围过宽 ​

路径匹配规则要有单元测试,尤其关注:

  • 通配符
  • 尾斜杠
  • URL 编码
  • 大小写
  • 参数段
  • 父子路径

8. 权限校验对象不稳定 ​

不要用展示名称作为 object:

text
“财务报表”

应使用稳定标识:

text
report:finance:monthly
order:123

13. 最小测试矩阵 ​

至少覆盖以下情况:

场景预期
直接命中允许策略allow
完全没有策略deny
用户通过角色获得权限allow
角色继承获得权限allow
角色存在但资源不匹配deny
action 不匹配deny
domain 不匹配deny
Pattern 匹配范围错误deny
ABAC 部门不匹配deny
ABAC 等级不足deny
显式 deny 与 allow 同时命中按 policy_effect 规则判断
Enforce 返回错误fail-closed
策略变更后其他实例按同步方案验证
未登录用户直接调用 API401
已登录但无权限403

推荐把权限测试写成表格驱动测试:

text
输入 subject / domain / object / action / attributes
预期 allowed / error

这样比只测试某一个管理员账号更容易发现边界问题。


15. 一句话记忆 ​

text
ACL:权限直接给人
RBAC:权限给角色,人继承角色
RBAC with domains:权限还要限定在哪个租户/组织/项目
RBAC with pattern:角色权限可以覆盖一类资源路径
deny-override:显式拒绝优先
ABAC:根据属性动态判断
ReBAC:根据关系判断

Casbin 的核心不是记住模型名字,而是理解:

text
Request 定义输入
Policy 定义规则
Role 定义关系
Matcher 定义计算
Effect 定义裁决

基于 VitePress 构建