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 至少两轮。更要命的是,下个季度还会有 compliancesupportreadonly_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 Casbin Mindmap

参考资料