PERM 模型与 Casbin:把云端授权从代码里抠出去
Posted on 二 02 6月 2026 in Tech
| Abstract | PERM 模型与 Casbin:把云端授权从代码里抠出去 |
|---|---|
| Authors | Walter Fan |
| Category | Cloud / Security |
| Status | v1.0 |
| Updated | 2026-06-02 |
| License | CC-BY-NC-ND 4.0 |
一个让人头大的早晨
某年某月的某一天,PM 跑过来说:"给客户新增一个角色叫 auditor,只能读所有租户的日志,不能改任何东西。下周一上线。"
你打开 IDE,心里咯噔一下。咱这服务里跟权限相关的判断,散落在三十多个 handler 里,长这副样子:
if user.Role == "admin" || (user.Role == "owner" && user.TenantID == log.TenantID) {
// ...
}
加一个角色,意味着这三十多处 if 都得重新审一遍,测试用例翻倍,code review 至少两轮。更要命的是,下个季度还会有 compliance、support、readonly_dev 一堆角色排队进来。
那么,能不能把"谁能干什么"这件事,从业务代码里彻底抠出去?
这个问题学术界早就有人琢磨过,工业界也有现成的轮子。理论叫 PERM,工具叫 Casbin。北大的 Luo Yang 等人在 2025 年发了篇 IEEE TIFS 的论文 《PERM: Streamlining Cloud Authorization With Flexible and Scalable Policy Enforcement》,算是把 Casbin 的设计哲学讲明白了。
咱们今天就来拆一拆这个 PERM。
一、PERM 是什么:把授权拆成四块拼图
PERM 是四个英文单词的首字母:Policy、Effect、Request、Matchers。一句话总结就是:
PERM 是一个授权元模型,它把"判断一次访问是否被允许"这件事,拆成四个正交的部分,每一部分都用配置文件描述。
为什么叫"元模型"?因为它本身不是 RBAC、不是 ABAC,而是一套能生成各种授权模型的语法。就像 BNF 是描述语言的语言,PERM 是描述授权模型的模型。
四块拼图各管一摊:
| 部件 | 管什么 | 一句话解释 |
|---|---|---|
| Request | 一次访问长啥样 | "谁,对什么,做什么",标准是三元组 (sub, obj, act) |
| Policy | 规则长啥样 | 策略的字段结构,比如 (sub, obj, act) 或带 effect 的 (sub, obj, act, eft) |
| Matchers | 怎么算"匹配上了" | 一个布尔表达式,决定 request 命中哪条 policy |
| Effect | 多条命中时怎么裁决 | 比如 "只要有一条 allow 就放行"、"有 deny 就否决" |
写成 .conf 文件,一个最简单的 ACL(访问控制列表)模型长这样:
[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
配套的策略文件 policy.csv:
p, alice, data1, read
p, bob, data2, write
意思很直白:alice 能读 data1,bob 能写 data2。Enforce 来个 ("alice", "data1", "read"),逐条对照 policy,matcher 全部 true,effect 说"有一条 allow 就放行",结果就是 true。
到这里你可能觉得:这不就是把 if-else 写成了配置文件吗?
固然如此,可是别急,PERM 的杀招在后面。
二、PERM 真正的杀招:换模型不换引擎
刚才那个 ACL 模型,咱们一行代码不动,只改 model.conf,就能升级成 RBAC:
[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
多了一个 [role_definition] 段 g = _, _,表示"用户—角色"是个二元关系。matcher 里的 r.sub == p.sub 也换成了 g(r.sub, p.sub),意思是"request 的 sub 是否拥有 policy 里的 sub 这个角色"。
policy.csv 变成:
p, admin, data1, read
p, admin, data1, write
g, alice, admin
alice 是 admin,admin 能读写 data1,所以 alice 也能读写 data1。
想要 ABAC?把 matcher 改成 r.sub.Age >= 18 && r.obj.Owner == r.sub.Name 即可。想要带租户隔离的 RBAC?加一个 domain 字段,matcher 里多一个 &&。
核心思想就这一条:业务代码永远只调一行 enforcer.Enforce(sub, obj, act),授权语义全部在配置文件里演化。
这就是为什么论文里把 PERM 叫做 "streamlining cloud authorization"——云端服务多租户、多角色、多场景,需求一天一个变,把变化收敛到配置层,代码层稳如老狗,运维和安全团队也能直接改策略,不用每次都拉开发陪跑。
三、上手 Casbin:Go 版三步走
理论讲完,咱们撸代码。Go 版 Casbin 叫 casbin/casbin,安装:
go get github.com/casbin/casbin/v2
第一步:写两个文件
model.conf(RBAC 模型):
[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
policy.csv:
p, admin, /logs, read
p, admin, /logs, write
p, auditor, /logs, read
g, alice, admin
g, bob, auditor
第二步:写业务代码
package main
import (
"fmt"
"log"
"github.com/casbin/casbin/v2"
)
func main() {
e, err := casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
log.Fatalf("load enforcer failed: %v", err)
}
cases := []struct {
sub, obj, act string
}{
{"alice", "/logs", "read"}, // admin → 应放行
{"alice", "/logs", "write"}, // admin → 应放行
{"bob", "/logs", "read"}, // auditor → 应放行
{"bob", "/logs", "write"}, // auditor 无写权限 → 应拒绝
{"carol", "/logs", "read"}, // 无角色 → 应拒绝
}
for _, c := range cases {
ok, err := e.Enforce(c.sub, c.obj, c.act)
if err != nil {
log.Printf("enforce error: %v", err)
continue
}
fmt.Printf("%-6s %-8s %-6s => %v\n", c.sub, c.obj, c.act, ok)
}
}
跑起来:
alice /logs read => true
alice /logs write => true
bob /logs read => true
bob /logs write => false
carol /logs read => false
第三步:动态改策略
线上加一个 auditor 不需要重启服务,调 API 即可:
// 加策略
_, _ = e.AddPolicy("auditor", "/metrics", "read")
// 把 carol 加入 auditor 角色
_, _ = e.AddGroupingPolicy("carol", "auditor")
// 持久化到 storage(前提是用了 Adapter,比如 GORM)
_ = e.SavePolicy()
生产环境一般会把 policy 存到 MySQL/PostgreSQL,用 casbin/gorm-adapter 之类的 Adapter;多实例之间用 Watcher 同步(Redis、etcd 都有现成的)。这套基础设施成熟得很,开箱即用。
四、Casbin vs OPA:选哪个不纠结
讲 Casbin 不提 OPA(Open Policy Agent) 是不厚道的。OPA 是 CNCF 毕业项目,K8s 生态里几乎是策略引擎的事实标准,自家有 Rego 语言。这俩经常被一起讨论,但定位不太一样:
| 维度 | Casbin | OPA |
|---|---|---|
| 设计哲学 | 元模型(PERM)+ 配置 DSL | 通用策略引擎 + 图灵完备的 Rego |
| 学习曲线 | 半天就能上手,几个段几条 matcher | 得专门学 Rego,类 Datalog 思维 |
| 嵌入方式 | 进程内库(每种语言一个原生实现) | 独立进程 / sidecar / WASM |
| 典型场景 | 应用内部权限:菜单、数据、API | 跨服务策略:K8s 准入、Envoy 鉴权、CI/CD 卡点 |
| 策略表达力 | 够用,但复杂逻辑要靠自定义函数 | 极强,可以写很重的规则 |
| 决策延迟 | 微秒级(in-process) | 毫秒级(IPC/HTTP) |
一句话选型:
- 应用内的多租户 RBAC / ABAC,跟业务紧耦合 → 优先 Casbin,省心。
- 跨服务、跨平台、需要统一策略平面(比如 K8s + 微服务 + CI 都要约束)→ 上 OPA,值得投入。
我自己的经验是:单个 Go 服务里的权限,Casbin 几乎永远是正确答案;一旦权限需要跨语言、跨进程统一管理,OPA 的架构红利就显现出来了。两者不是替代关系,是不同层级的工具。
五、几个坑,请提前避开
用过两年 Casbin,踩过几个坑,提前告诉你:
坑 1:matcher 表达式的顺序影响性能
官方文档明确提到过这事。matcher 是按从左到右短路求值的,把贵的条件(比如 g(r.sub, p.sub) 这种角色查找)放在便宜的字符串比较后面,能差出几个数量级的延迟。Casbin 维护者跑过一个有 2500 个项目、每个项目 4 个角色的压测,matcher 顺序写错,单次 enforce 慢到 6 秒;调整顺序后回到几毫秒。
写法是这样:
# 慢
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
# 快
m = r.obj == p.obj && r.act == p.act && g(r.sub, p.sub)
坑 2:policy 字段都是字符串
policy.csv 里的所有字段,进了 Casbin 都当字符串处理。想塞个 age >= 18 进去?得自己写 helper 函数,或者用 ABAC 把对象传进 request,让 matcher 里去比。别指望 p, alice, data1, 100 里那个 100 是 int。
坑 3:策略变更要广播
单实例没问题,多实例部署时,A 实例改了 policy,B 实例还用着内存里的老版本。必到用 Watcher(Redis Pub/Sub 是最常见的方案),或者所有写操作走中心化的 Admin Portal。
坑 4:别把策略写成代码的镜像
见过有人把每个 API 都写一条 policy:p, alice, /api/v1/users/:id/profile, GET。这种粒度的策略,本质还是把 if-else 搬进了 CSV,反而失去了抽象的意义。策略要按业务概念组织,不是按 URL。
六、什么时候用 Casbin:5 条 CheckList
最后给一个判断清单,符合 3 条以上就值得引入:
- [ ] 系统有 2 个以上角色,且未来还会增加
- [ ] 权限规则需要非开发人员(产品、安全、运维)也能改
- [ ] 同一份代码要支持多种部署形态(单租户、多租户、私有化)
- [ ] 审计要求能追溯每一次访问决策的依据
- [ ] 权限模型可能演化(从 RBAC 升级到 ABAC、加上 domain、加上属性等)
反过来,如果你的系统只有"登录用户"和"管理员"两种人,权限规则 5 条以内一辈子不变,那一段 if-else 比啥都强,别为了用而用。
收束:把变化关进配置文件
那天下午,我把那三十多处 if 全删了,换成一行 enforcer.Enforce(user.Name, resource, action)。新加 auditor 角色的工作量,从两天降到了二十分钟,主要还是用在跟 PM 对齐有哪些资源该读、哪些不该读。
PERM 这套设计的精髓,我后来跟同事是这么讲的:
代码负责"如何执行",配置负责"是否允许"。两件事分开,世界清净。
授权这个领域,最怕的不是规则复杂,是规则变。PERM 的贡献,就是把变化挪到了一个可控、可审计、可热更新的地方。这个思路本身比 Casbin 这个具体实现要值钱得多——哪怕你用 OPA、用 Cedar、自己撸一个,把"模型—策略—执行"三者解耦的方向都是对的。
总结脑图
@startmindmap perm_casbin_mindmap
* PERM 与 Casbin
** Request
*** sub / obj / act
*** 一次访问的快照
** Policy
*** sub / obj / act (/ eft)
*** 规则的字段结构
** Matchers
*** 布尔表达式
*** 决定是否命中
*** 顺序影响性能
** Effect
*** allow-override
*** deny-override
*** 多策略裁决
** 落地建议
*** 单服务用 Casbin
*** 跨平台用 OPA
*** 策略按业务概念组织
*** 别按 URL 切片
** 避坑清单
*** matcher 顺序
*** 字段都是字符串
*** 多实例用 Watcher
@endmindmap

参考资料
- 论文:PERM: Streamlining Cloud Authorization With Flexible and Scalable Policy Enforcement
- Casbin 官方文档:https://casbin.apache.org/docs/tutorials/
- Casbin 在线编辑器:https://editor.casbin.org/
- Open Policy Agent:https://www.openpolicyagent.org/