核心概念
Casbin 的一切都建立在 PERM 元模型(Policy, Effect, Request, Matchers)之上。理解了 PERM,就能读懂任何 model.conf,也能自己设计模型。本章逐段拆解 model.conf 的语法,并解释一次 Enforce() 调用内部发生了什么。
PERM 元模型总览
Request(请求)──┐
├─→ Matcher(匹配器)──→ 每条策略得到匹配结果
Policy(策略)───┘ │
▼
Effect(效果器):汇总所有匹配结果
│
▼
true / false| 原语 | model.conf 中的段 | 作用 |
|---|---|---|
| Request | [request_definition] |
定义 Enforce() 的入参形状 |
| Policy | [policy_definition] |
定义一条策略的字段 |
| Matchers | [matchers] |
布尔表达式:请求与某条策略是否匹配 |
| Effect | [policy_effect] |
把"每条策略的匹配结果"汇总成最终决定 |
| (可选)Role | [role_definition] |
RBAC 角色继承关系 |
🔬 深入原理:为什么要抽象出 PERM?传统权限库把模型硬编码(比如只支持 RBAC),换需求就要换库。Casbin 把"模型"本身做成配置:ACL、RBAC、ABAC 只是 PERM 的不同实例化。这就是它一个库能覆盖几乎所有访问控制场景的原因。
[request_definition] 请求定义
[request_definition]
r = sub, obj, act- 定义
Enforce()的参数个数与名字。r = sub, obj, act意味着必须e.Enforce(sub, obj, act)传 3 个参数。 - 字段名可自定义,只要与 matcher 中引用一致:
r = sub, dom, obj, act # 多租户:加一个 domain
r = sub, obj # 只关心"谁访问什么",不关心操作🚨 陷阱:Enforce() 的实参个数、顺序必须与 request_definition 完全一致。传错个数返回 error;传错顺序则悄悄得到错误结果(Enforce("data1", "alice", "read") 不会报错,只会永远 false)。
[policy_definition] 策略定义
[policy_definition]
p = sub, obj, act- 定义策略每行的字段。CSV 中的
p, alice, data1, read依序对应p.sub, p.obj, p.act。 - 可以定义多种策略:
p = sub, obj, act
p2 = sub, act # 第二种策略形状,matcher 里用 p2.sub 引用隐藏字段 p.eft
每条策略末尾都有一个隐式字段 eft(effect),取值 allow(默认)或 deny:
[policy_definition]
p = sub, obj, act, eftp, alice, data1, read, allow
p, alice, data1, write, deny不显式声明 eft 时,所有策略默认为 allow。eft 配合 policy_effect 实现"黑名单/拒绝优先"(见下文)。
[policy_effect] 效果器
matcher 对每一条策略求值后,effect 决定如何汇总。Casbin 内置支持以下几种表达式(只能用内置的这几种,不支持任意表达式):
| 表达式 | 名称 | 语义 |
|---|---|---|
some(where (p.eft == allow)) |
allow-override | 任意一条 allow 策略匹配 → 放行(白名单,最常用) |
!some(where (p.eft == deny)) |
deny-override | 没有任何 deny 策略匹配 → 放行(黑名单:默认全放行,只挡黑名单) |
some(where (p.eft == allow)) && !some(where (p.eft == deny)) |
allow-and-deny | 至少一条 allow 且无任何 deny → 放行(白名单+一票否决) |
priority(p.eft) || deny |
priority | 按策略顺序,第一条匹配的策略说了算 |
subjectPriority(p.eft) || deny |
主体优先级 | 按角色继承层级,越具体的主体优先级越高 |
💡 最佳实践:需要"管理员有所有权限,但某人被单独禁用某操作"时,用 allow-and-deny + eft 字段,比在业务代码里打补丁优雅得多:
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))p, admin, data1, write # admin 组可写
p, alice, data1, write, deny # 但 alice 被单独禁止(即使她是 admin)
g, alice, admin[matchers] 匹配器
matcher 是一个布尔表达式,决定"这条请求与这条策略是否匹配":
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act支持的语法(底层是 govaluate 表达式引擎):
| 类别 | 语法 | 示例 |
|---|---|---|
| 逻辑 | && || ! |
r.sub == p.sub || r.sub == "root" |
| 比较 | == != > < >= <= |
r.sub.Age >= 18(ABAC) |
| 集合 | in |
r.obj in ("data1", "data2") |
| 算术 | + - * / % |
少用,但支持 |
| 函数 | keyMatch(...) 等 |
keyMatch2(r.obj, p.obj),见 06-匹配器与函数 |
| 角色 | g(...) |
g(r.sub, p.sub),有 role_definition 时可用 |
多个 matcher:可定义 m2、m3,用 e.EnforceWithMatcher("m2 的表达式", ...) 或 EnforceEx 场景下动态指定。绝大多数项目只用一个 m。
🚨 陷阱:matcher 中引用了 policy_definition 里不存在的字段(如写了 p.dom 但 p 没定义 dom)会在 Enforce 时报错。改模型时两边要同步。
[role_definition] 角色定义(RBAC 专用)
[role_definition]
g = _, _ # (用户, 角色)
g2 = _, _ # 第二个分组关系,常用于"资源分组"
g3 = _, _, _ # 三个 _ :带 domain 的角色关系 (用户, 角色, 域)g = _, _声明一个名为g的分组关系(grouping),每个_是一个参数位- matcher 中
g(r.sub, p.sub)表示 “r.sub 直接或间接继承了 p.sub 角色” g是传递的:g, alice, admin+g, admin, superadmin⇒g(alice, superadmin)为 true- 详见 03-RBAC与多租户
🔬 深入原理:一次 Enforce 的完整流程
ok, err := e.Enforce("alice", "data1", "read")- 参数绑定:按
request_definition把实参绑定到r.sub = "alice",r.obj = "data1",r.act = "read" - 遍历策略:对内存中每一条 p 策略,把
p.sub/p.obj/p.act代入 matcher 表达式求值- matcher 中的
g(...)调用会查询 RoleManager 内部的角色继承图(预构建,查询近似 O(1)~O(层数))
- matcher 中的
- 收集效果:每条策略得到 匹配+allow / 匹配+deny / 不匹配 三种结果之一
- effect 汇总:按
policy_effect规则汇总为最终 true/falsesome(where (p.eft == allow))在遇到第一条 allow 匹配时短路返回,不会遍历完所有策略
⚡ 性能提示:Enforce 的复杂度约为 O(策略条数 × matcher 复杂度)。策略上万条时应使用 Filtered Adapter 只加载相关子集,或用 CachedEnforcer 缓存判定结果(见 08-分布式与高性能)。
Enforcer 家族
| 类型 | 构造函数 | 并发安全 | 适用场景 |
|---|---|---|---|
Enforcer |
NewEnforcer |
❌ | 单线程、脚本、策略只读 |
SyncedEnforcer |
NewSyncedEnforcer |
✅(读写锁) | Web 服务默认选择:运行时会增删策略 |
CachedEnforcer |
NewCachedEnforcer |
✅ | 判定结果缓存,读多写少、策略量大 |
SyncedCachedEnforcer |
NewSyncedCachedEnforcer |
✅ | 上面两者结合 |
DistributedEnforcer |
NewDistributedEnforcer |
✅ | 配合 Dispatcher(如 Raft)做强一致集群 |
e, err := casbin.NewSyncedEnforcer("model.conf", adapter)💡 最佳实践:只要是 HTTP/gRPC 服务,直接用 SyncedEnforcer。裸 Enforcer 在"一边 Enforce 一边 AddPolicy"时会出现 data race(go test -race 可复现)。
常用判定 API 变体
| API | 说明 |
|---|---|
Enforce(rvals...) (bool, error) |
标准判定 |
EnforceEx(rvals...) (bool, []string, error) |
额外返回命中的那条策略,调试/审计利器 |
BatchEnforce(requests [][]any) ([]bool, error) |
批量判定,一次锁开销处理多个请求 |
EnforceWithMatcher(matcher string, rvals...) |
用临时 matcher 判定,不改模型 |
ok, reason, _ := e.EnforceEx("alice", "data1", "read")
// ok = true, reason = ["alice", "data1", "read"] —— 命中的策略行model.conf 完整语法速查
# 注释以 # 开头
[request_definition]
r = sub, obj, act # 可定义 r2, r3...
[policy_definition]
p = sub, obj, act # 可定义 p2, p3...;隐含 eft 字段
[role_definition] # 可选,RBAC 才需要
g = _, _ # 可定义 g2, g3...;_ 的个数 = 参数个数
[policy_effect]
e = some(where (p.eft == allow)) # 只能用内置的 5 种表达式
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
# 表达式太长可用 \ 换行,或拆成多个条件用 && 连接常见陷阱
🚨 effect 表达式不能自创:[policy_effect] 只支持内置的 5 种写法(字符串精确匹配),写 some(where (p.eft != deny)) 之类的变体会直接报错 “unsupported effect”。
🚨 matcher 与 policy 字段错位:policy_definition 加了字段(如 eft 或 dom),CSV/数据库里的旧策略行没有对应列,加载时报字段数不匹配,或者旧数据整体错位一格。改模型字段 = 迁移策略数据。
🚨 == 是精确匹配:r.obj == p.obj 不会处理 /api/users/* 这种通配。要通配必须用 keyMatch 系列函数(见 06-匹配器与函数)。新手常把通配符写进策略却用 == 匹配,结果永远 false。
🚨 priority 效果器依赖策略顺序:priority(p.eft) || deny 下,谁排在前面谁生效。用数据库 adapter 时行的加载顺序不保证与插入顺序一致,需要用显式优先级字段模型(见 02-访问控制模型大全)。
🚨 裸 Enforcer 并发不安全:多 goroutine 同时读(Enforce)是安全的,但读与写(AddPolicy/LoadPolicy)并发会 race。Web 服务一律 SyncedEnforcer。