Skip to content
Go
核心概念

核心概念

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, eft
p, alice, data1, read, allow
p, alice, data1, write, deny

不显式声明 eft 时,所有策略默认为 alloweft 配合 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:可定义 m2m3,用 e.EnforceWithMatcher("m2 的表达式", ...)EnforceEx 场景下动态指定。绝大多数项目只用一个 m

🚨 陷阱:matcher 中引用了 policy_definition 里不存在的字段(如写了 p.domp 没定义 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, superadming(alice, superadmin) 为 true
  • 详见 03-RBAC与多租户

🔬 深入原理:一次 Enforce 的完整流程

ok, err := e.Enforce("alice", "data1", "read")
  1. 参数绑定:按 request_definition 把实参绑定到 r.sub = "alice", r.obj = "data1", r.act = "read"
  2. 遍历策略:对内存中每一条 p 策略,把 p.sub/p.obj/p.act 代入 matcher 表达式求值
    • matcher 中的 g(...) 调用会查询 RoleManager 内部的角色继承图(预构建,查询近似 O(1)~O(层数))
  3. 收集效果:每条策略得到 匹配+allow / 匹配+deny / 不匹配 三种结果之一
  4. effect 汇总:按 policy_effect 规则汇总为最终 true/false
    • some(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 加了字段(如 eftdom),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