核心架构

核心特性

将授权逻辑与业务代码分离,通过配置文件实现复杂的权限模型(RBAC、ABAC 等)

混合访问控制模型

Casbin 基于 PERM 元模型 (Policy, Effect, Request, Matchers) 来定义访问控制规则

组件 描述
Request 访问请求的结构(谁、想做什么、 ресурс)
Policy 权限规则定义
Matchers 匹配规则,决定请求如何对应到策略
Effect 最终效果(允许/拒绝)

通过修改 CONF 配置文件即可切换授权机制,无需改动代码

灵活的策略存储

支持多种存储方式

存储 描述
内存 用于测试
文件 JSON/YAML/CSV
数据库 MySQL、Postgres、MongoDB、Redis、Cassandra、S3

跨语言跨平台

  1. 各语言实现相同的 API 和行为
  2. Go、Java、PHP、Node.js、Python、.NET、Rust 等

扩展机制

Casbin 核心保持轻量,策略存储、角色管理都通过插件化扩展,按场景选择合适的 Adapter 和 Role Manager

Policy 持久化

通过 Adapter(适配器) 将策略存储与核心库分离

  1. 核心库只负责策略执行
  2. 存储逻辑由独立的 Adapter 实现
  3. 支持文件、数据库、S3 等多种后端
  4. 默认内置文件 Adapter,其他需第三方提供

大规模策略执行

当策略数据量很大时,全量加载效率低下

  1. Filtered Policy Loading - 支持从存储中只加载符合过滤条件的策略子集
  2. 典型场景:多租户应用、角色分级等
  3. 避免内存溢出,提升性能

Role Manager

专门处理 RBAC 角色层级(用户-角色映射)

  1. 可从 Casbin 策略规则中加载角色关系
  2. 也可从外部源(LDAP、Okta、Auth0、Azure AD 等)同步
  3. 同样与核心库分离,按需选用

Overview

Casbin 是一个高效的开源访问控制库,支持多种授权模型

核心组件

Enforcer 根据 Model 的规则,用 Policy 中的定义,来判断每个请求是否被允许

组件 作用
Model 定义授权逻辑(布局、执行流程、条件)
Policy 定义具体规则(谁、对什么资源、做什么操作)
Enforcer 核心执行器,用 Model + Policy 评估每个请求

简单请求

请求 → Enforcer → 匹配 Model 和 Policy → 允许/拒绝

核心特点

特点 描述
规则驱动 在 Policy 文件中定义 主体-对象-操作 规则
格式灵活 Policy 文件可以是 JSON、YAML、CSV 等任意格式
模型控制 Model 文件让管理员完全掌控授权逻辑
跨语言一致 所有语言实现(Go/Java/Python 等)行为统一

语言支持

生产级状态

Go、Java、Node.js、PHP、Python、C#、C++、Rust

核心功能对比

Casbin 目标实现全语言功能对等,但多线程、Watcher、Role Manager 等高级功能尚未完全统一

功能 说明
Enforcement 请求拦截与决策(全部支持)
RBAC 基于角色的访问控制(全部支持)
ABAC 基于属性的访问控制(全部支持)
Adapter 策略存储适配器(Delphi/Lua/Dart 不支持)
Role Manager 角色管理层(Delphi 不支持)
Multi-Threading 多线程支持(仅 Go/Java/Python/Rust 支持)

定义

Casbin 是一个授权库,处理标准的 { subject, object, action } 访问控制模型,也支持 RBAC、ABAC 等复杂场景

元素 含义 示例
Subject 请求方(用户或服务) alice, service-account
Object 资源(被访问的对象) file.txt, /api/users
Action 操作(对资源的动作) read, write, delete

谁(subject)对什么(object)做什么操作(action)是否被允许?

能力

功能 说明
策略执行 在 { subject, object, action } 格式或自定义格式下执行允许/拒绝
存储管理 管理访问控制模型和策略的存储
角色层级 处理用户-角色、角色-角色关系(RBAC 角色继承)
超级用户 内置超级用户(如 root)拥有无限制访问权限
模式匹配 内置匹配运算符(如 keyMatch 支持 /foo* 模式)

非能力

应用已有用户管理系统,Casbin 专注于授权决策,不涉及身份认证

功能 说明
用户认证 不验证用户名/密码等登录凭证
用户/角色管理 不维护用户列表或角色列表

Get Started

Installation

1
go get github.com/casbin/casbin/v3

Example

元素 角色
Model 定义规则结构
Adapter 提供策略存储
Policy 定义具体权限
Enforcer 执行权限判断

model.conf

1
2
3
4
5
6
7
8
9
10
11
[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

1
2
3
p, alice, data1, read
p, bob, data2, write
p, alice, data2, write

main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import (
"fmt"
"log"

"github.com/casbin/casbin/v3"
)

func main() {
// 创建 Enforcer(使用文件方式)
e, err := casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
log.Fatalf("创建 Enforcer 失败: %v", err)
}

// 单个权限检查
check(e, "alice", "data1", "read") // ✅ 允许
check(e, "alice", "data1", "write") // ❌ 拒绝
check(e, "bob", "data2", "write") // ✅ 允许

// 批量权限检查
results, err := e.BatchEnforce([][]interface{}{
{"alice", "data1", "read"},
{"bob", "data2", "write"},
{"alice", "data2", "write"},
})
if err != nil {
log.Fatalf("批量检查失败: %v", err)
}
fmt.Printf("批量结果: %v\n", results) // [true true true]

// 获取用户角色
roles, _ := e.GetRolesForUser("alice")
fmt.Printf("alice 的角色: %v\n", roles) // []
}

func check(e *casbin.Enforcer, sub, obj, act string) {
ok, err := e.Enforce(sub, obj, act)
if err != nil {
log.Printf("检查失败: %v", err)
return
}
if ok {
fmt.Printf("✅ %s 可以 %s %s\n", sub, act, obj)
} else {
fmt.Printf("❌ %s 不能 %s %s\n", sub, act, obj)
}
}

Enforcer 是核心组件,需要 Model + Adapter 两个配置

1
2
3
4
5
6
# 方式一:文件方式(简单场景)
e, _ := casbin.NewEnforcer("model.conf", "policy.csv")

# 方式二:自定义 Model + 数据库 Adapter
// 使用 MySQL 存储策略
a, _ := xormadapter.NewAdapter("mysql", "连接字符串")

运行结果

1
2
3
4
5
✅ alice 可以 read data1
❌ alice 不能 write data1
✅ bob 可以 write data2
批量结果: [true true true]
alice 的角色: []

How It Works

PERM 四个组件共同定义授权逻辑

Request 定义请求格式 → Policy 定义权限格式 → Matchers 定义如何匹配 → Effect 定义最终允许/拒绝

组件 作用 示例
Request 定义请求参数结构 r = sub, obj, act
Policy 定义策略规则结构 p = sub, obj, act
Matchers 请求与策略的匹配逻辑 r.sub == p.sub && r.obj == p.obj && r.act == p.act
Effect 多个策略匹配后的最终效果 some(where (p.eft == allow))

Effect 表达式

1
2
3
4
5
# 匹配到任意一个 allow 就允许
e = some(where (p.eft == allow))

# 必须有 allow 且不能有 deny(deny 优先)
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

Model (model.conf)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[request_definition]
r = sub, obj, act
请求格式:r = (谁, 什么资源, 什么操作)

[policy_definition]
p = sub, obj, act
策略格式:p = (谁, 什么资源, 什么操作)

[policy_effect]
e = some(where (p.eft == allow))
效果:匹配到任意一条 allow 策略就允许

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
匹配逻辑:请求的 subject/obj/act 必须与策略完全一致

Policy (policy.csv)

1
2
p, alice, data1, read  # alice 可以读 data1
p, bob, data2, write # bob 可以写 data2

匹配过程

1
2
3
4
5
6
7
alice 读 data1
↓
Request: (alice, data1, read)
↓
Matcher: alice==alice && data1==data1 && read==read → 匹配成功
↓
Effect: 匹配到 p.eft=allow → 允许

Matchers 多行写法

1
2
3
[matchers]
m = r.sub == p.sub && r.obj == p.obj \
&& r.act == p.act

in 操作符(仅 Go)

1
2
3
m = r.obj == p.obj && r.act == p.act || r.obj in ('data2', 'data3')

⚠️ in 操作符的数组必须有多于一个元素,否则可能 panic

Access Control Models

Casbin 支持业界主流的访问控制模型,RBAC 和 ABAC 最常用,内置模型如下

模型 说明
ACL 基础访问控制列表(用户-资源-操作)
RBAC 基于角色的访问控制(支持角色层级)
ABAC 基于属性的访问控制(用户属性、资源属性、环境属性)
ReBAC 基于关系的访问控制(实体间关系)
PBAC 基于策略的访问控制
MAC 强制访问控制(多级安全)
OrBAC 组织-Based 访问控制
UCON 使用控制模型(包含义务和条件)

特色功能

功能 说明
Priority Model 支持策略优先级,优先级高的生效
Super Administrator 超级管理员(如 root)拥有无限制访问权限
RBAC with Domains 多租户/多域场景的角色管理

Supported Models

从简单 ACL 到复杂 BLP/LBAC,Casbin 覆盖了几乎所有访问控制模型

ACL 系列

模型 说明 适用场景
ACL 基础模型:用户-资源-操作 简单权限管理
ACL with superuser 支持超级管理员(如 root) 需要最高权限账号
ACL without users 无用户概念,适合 API/设备访问 物联网、API 网关
ACL without resources 按资源类型而非实例授权 “写文章”权限而非”写某篇文章”

RBAC 系列

模型 说明 适用场景
RBAC 基础角色模型 常规权限管理
RBAC with resource roles 用户和资源都有角色 部门资源隔离
RBAC with domains 多域/多租户角色 SaaS、跨国企业

ABAC 及高级模型

模型 说明
ABAC 用属性(如 resource.Owner)做判断
PBAC 动态上下文策略
BLP 机密性模型(Bell-LaPadula)
Biba 完整性模型(防篡改)
LBAC 机密性+完整性 lattice 模型
OrBAC 多组织抽象层
UCON 持续授权、可变属性、义务、条件

专用模型

模型 说明
RESTful 路径模式匹配 /res/*, HTTP 方法控制
IP Match IP 地址或 CIDR 网段控制
Deny-override deny 优先于 allow
Priority 规则有序,先匹配先赢

Model Syntax

  1. Casbin 的 Model 就是一份声明式的权限元模型
    • 用配置描述”怎么问(request)、怎么配(policy)、怎么判(matcher)、怎么组合(effect)“
  2. 切换权限模型只需换 .conf 文件,Enforce() 调用保持不变,实现了权限逻辑与业务代码的彻底解耦
  3. Casbin 用一个 .conf 文件声明式地描述整个权限模型,核心是 PERM 框架

整体结构 - 一个 Model 文件必须有 4 个段落,RBAC 场景可额外加 1~2 个

段落 是否必需 用途
[request_definition] 必须 定义请求入参
[policy_definition] 必须 定义策略规则结构
[policy_effect] 必须 多条策略命中时如何组合结果
[matchers] 必须 请求与策略的匹配表达式
[role_definition] RBAC 必须 定义角色继承关系
[constraint_definition] 可选 RBAC 约束(职责分离等)

Request Definition - 请求定义

1
2
[request_definition]
r = sub, obj, act

定义 e.Enforce(…) 的入参签名,经典三元组

元祖 说明
sub 主体(谁)
obj 客体(操作什么资源)
act 动作(做什么)

可以自定义:不需要资源就写 r = sub, act,两个主体就写 r = sub, sub2, obj, act

Policy Definition - 策略定义

1
2
3
[policy_definition]
p = sub, obj, act
p2 = sub, act

描述策略文件的列结构,策略文件中的每一行:

1
2
p, alice, data1, read       → 绑定 (p.sub=alice, p.obj=data1, p.act=read)
p2, bob, write-all-objects → 绑定 (p2.sub=bob, p2.act=write-all-objects)

关键点

  1. 每行第一个 token 是策略类型(p, p2),**必须与定义匹配**
  2. 所有元素都是字符串,不做类型转换
  3. p.eft 字段是策略效果(allow/deny),**省略时默认 allow**

Policy Effect - 策略效果

当多条策略同时命中时,如何组合结果,Casbin 内置了 5 种效果,**不支持自定义表达式**,只能用以上内置的

表达式 含义 适用场景
some(where (p.eft == allow)) allow-override:任一允许即允许 ACL, RBAC 等
!some(where (p.eft == deny)) deny-override:任一拒绝即拒绝 安全优先场景
some(where (p.eft == allow)) && !some(where (p.eft == deny)) allow-and-deny:需至少一个 allow 且无 deny 严格场景
`priority(p.eft) deny`
subjectPriority(p.eft) 角色优先级:按角色层级判断 基于角色的优先级

Constraint Definition - 约束定义(可选)

这是 RBAC 的高级特性,在角色分配变更时强制检查约束,防止违反安全规则,有 4 种约束类型,执行时机如下

  1. 在 AddGroupingPolicy() / RemoveGroupingPolicy() 时检查,违反约束则操作失败、策略不变
  2. 模型加载时也会全量校验现有数据

静态职责分离 (sod)

同一用户不能同时持有两个角色,例如:申请人和审批人不能是同一个人

1
c = sod("finance_requester", "finance_approver")

基数限制 (sodMax)

从一个角色集合中,同一用户最多只能持有 N 个,例如:薪资的查看、编辑、审批三个角色,一个人最多只能有一个

1
c2 = sodMax(["payroll_view", "payroll_edit", "payroll_approve"], 1)

角色基数 (roleMax)

一个角色最多只能分配给 N 个用户,例如:超级管理员最多 2 人

1
c3 = roleMax("superadmin", 2)

前置角色 (rolePre)

必须先持有前置角色才能被分配目标角色,例如:必须有安全培训认证才能成为数据库管理员

1
c4 = rolePre("db_admin", "security_trained")

Matchers - 匹配器

定义请求参数如何与策略字段匹配,支持算术(+ - * /)和逻辑运算符(&& || !)

1
2
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

性能关键:表达式顺序

原则:把廉价条件放前面,昂贵操作(如 g() 角色查找)放后面,利用短路求值减少计算量

慢写法

角色查找在前 - 测试中 jasmine 有 2500 个角色,g() 先执行导致 2500 次角色遍历,耗时 6.16 秒

1
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

快写法

简单的字段比较在前 - 先用 r.obj == p.obj 过滤掉大量不匹配的策略,再做昂贵的角色查找,耗时仅 7.29 ms,快了约 800 倍

1
m = r.obj == p.obj && g(r.sub, p.sub) && r.act == p.act

多组定义(Multiple Section Types)

可以定义多套 r/p/e/m,用数字后缀区分 - 适用于同一系统中有多种权限模型的场景(比如普通用户用 RBAC,管理员用 ABAC)

1
2
3
4
r2 = sub, obj, act
p2 = sub, obj, act
e2 = !some(where (p.eft == deny))
m2 = r.sub == p.sub && r.obj == p.obj && r.act == p.act

使用时通过 EnforceContext 指定用哪一套

1
2
3
4
5
ctx := NewEnforceContext("2")  // 使用 r2, p2, e2, m2
e.Enforce(ctx, alice, data1, read)

// 不传 ctx 就用默认的 r, p, e, m
e.Enforce(alice, data2, read)

in 操作符

判断左侧值是否在右侧数组中(严格 ==,无类型转换),适用于类似”用户是否在管理员列表中”的场景

1
m = r.sub.Name in (r.obj.Admins)

表达式引擎

Casbin 底层依赖各语言的表达式引擎来解析 matcher - 不同语言的引擎能力有差异,如果需要跨语言兼容,只用文档中描述的标准语法

实现 语言 引擎
Casbin Go govaluate
jCasbin Java aviatorscript
Node-Casbin Node.js expression-eval
PyCasbin Python simpleeval

Effector

Effector 是权限判定流程中的最后一步,负责把多条匹配策略的结果合并为一个最终决定

核心概念

  1. 当一次 Enforce() 调用时,可能同时命中多条策略(比如一条 allow、一条 deny),Effector 就是决定”怎么拍板“的组件
  2. 请求 → Matcher 匹配 → 多条策略命中,各有效果 → Effector 合并 → 最终决定

MergeEffects() - 合并效果

这是 Effector 的核心方法

1
Effect, explainIndex, err = e.MergeEffects(expr, effects, matches, policyIndex, policyLength)

参数

参数 含义
expr 策略效果表达式,即 model 中 [policy_effect] 段定义的字符串,如 some(where (p.eft == allow))
effects 有匹配策略的效果切片,每个元素是 Allow、Deny 或 Indeterminate(不确定)
matches 布尔数组,标记哪些策略被匹配到了
policyIndex 当前评估到第几条策略
policyLength 策略总数

返回值

返回值 含义
Effect 最终决定:Allow、Deny 或 Indeterminate
explainIndex 是哪条策略决定了结果(用于审计/调试),无法确定时为 -1
err 如果效果表达式不支持,返回错误

合并逻辑举例

以 allow-override 为例(some(where (p.eft == allow)))

1
2
3
策略1: allow   ← 命中
策略2: deny ← 命中
策略3: allow ← 未命中
  1. effects = [Allow, Deny],Effector 的逻辑:只要有任意一条 allow,且没有 deny → 最终 allow
  2. 实际结果是 allow

不同效果表达式的合并规则

效果类型 合并逻辑
allow-override 任一 allow → allow
deny-override 任一 deny → deny
allow-and-deny 至少一个 allow 且无 deny → allow
priority 按策略顺序,先命中者胜出

短路优化

除非效果表达式是 priority(p_eft) || deny,否则 enforcer 可以短路:一旦结果确定,剩余策略就不再评估

1
2
3
4
5
6
效果表达式: some(where (p.eft == allow))   // allow-override
共 1000 条策略

策略 #1: deny
策略 #2: allow ← 到这里结果已确定为 allow,短路退出
策略 #3 ~ #1000: 跳过,不评估
  1. 这就是为什么 policyIndex 可能小于 policyLength - 1 - 说明后面的策略被跳过了
  2. 对于 priority 效果则不能短路,因为必须按顺序遍历找到优先级最高的那条

自定义 Effector

可以实现自己的 Effector 接口,定义自定义的合并语义 - 适用于内置效果表达式无法满足的场景

1
2
var e Effector  // 自定义实现
Effect, explainIndex, err = e.MergeEffects(expr, effects, matches, policyIndex, policyLength)
  1. Casbin 内置的 Default Effector 已经能处理所有内置效果表达式
  2. 只有需要完全自定义的合并逻辑时,才需要实现自己的 Effector

与 Model 中 Policy Effect 的关系

Model 中的 [policy_effect] 和这里的 Effector 是同一事物的两面

维度 说明
Model 层 用声明式字符串描述效果语义(如 some(where (p.eft == allow)))
代码层 Effector 组件实际执行这个语义,遍历效果切片并产出最终决定

Functions

让 Matcher 的表达能力从简单的 == 比较扩展到了模式匹配、正则、IP 网段、路径参数提取等丰富的场景,同时保持配置文件的声明式风格

类别 描述
匹配函数(keyMatch 系列) 解决”请求的 URL 是否匹配策略中的路径模式“
提取函数(keyGet 系列) 解决”从 URL 中提取参数值供后续使用“
自定义函数 解决内置函数覆盖不到的业务特定匹配逻辑(如时间范围、属性比较等)

内置函数总览

Matcher 不只能写 ==、&& 这种简单表达式,还可以调用内置函数来处理复杂的匹配场景,分两大类

Key Matching - 键匹配函数(返回 bool)

用于判断 URL/路径是否匹配某个模式

函数 通配符风格 示例 pattern 典型场景
keyMatch * /alice_data/* 简单通配符,* 匹配剩余所有字符
keyMatch2 :name /alice_data/:resource 类似 Express/Flask 的路径参数
keyMatch3 {name} /alice_data/{resource} 类似 OpenAPI/Spring 的路径参数
keyMatch4 {name} /data/{id}/book/{id} 同 keyMatch3 但支持同名参数一致性校验(两个 {id} 必须相同)
keyMatch5 {name} + * /data/{id}/* keyMatch3 和 keyMatch 的混合,还支持 query string
regexMatch 正则表达式 ^/alice_.*$ 任意正则匹配
ipMatch IP/CIDR 192.168.2.0/24 IP 地址或网段匹配
globMatch glob 模式 /alice_data/* 标准 glob 匹配(比 keyMatch 更通用的通配符)

具体例子

1
2
3
4
5
6
keyMatch("/alice_data/resource1", "/alice_data/*")            → true
keyMatch2("/alice_data/resource1", "/alice_data/:resource") → true
keyMatch3("/alice_data/resource1", "/alice_data/{resource}") → true
keyMatch4("/data/123/book/123", "/data/{id}/book/{id}") → true(两个 123 一致)
keyMatch4("/data/123/book/456", "/data/{id}/book/{id}") → false(123 ≠ 456)
ipMatch("192.168.2.123", "192.168.2.0/24") → true

Key Extraction - 键提取函数(返回 string)

不仅判断是否匹配,还提取匹配部分的值

1
2
3
keyGet2("/resource1/action", "/:res/action", "res")         → "resource1"
keyGet3("/resource1_admin/action", "/{res}_admin/*", "res") → "resource1"
keyGet("/resource1/action", "/*") → "resource1/action"

区别

函数 对应的匹配函数 用途
keyGet keyMatch 从 ***** 模式中提取匹配的片段
keyGet2 keyMatch2 从 :name 模式中提取指定参数名
keyGet3 keyMatch3 从 {name} 模式中提取指定参数名

用途举例:在 matcher 中提取出资源 ID,然后传给其他函数做进一步判断(比如检查资源归属)

自定义函数四步走

当内置函数不够用时,可以注册自己的函数

Step 1:实现实际逻辑

普通的 Go 函数,输入输出类型明确

1
2
3
4
5
6
7
8
9
10
func KeyMatch(key1 string, key2 string) bool {
i := strings.Index(key2, "*")
if i == -1 {
return key1 == key2
}
if len(key1) > i {
return key1[:i] == key2[:i]
}
return key1 == key2[:i]
}

Step 2:包装为 Casbin 签名

Casbin 的表达式引擎要求所有函数统一签名 func(...interface{}) (interface{}, error),所以需要一个适配层

1
2
3
4
5
func KeyMatchFunc(args ...interface{}) (interface{}, error) {
name1 := args[0].(string)
name2 := args[1].(string)
return (bool)(KeyMatch(name1, name2)), nil
}

Step 3:注册到 Enforcer

给函数起个名字,注册进去

1
e.AddFunction("my_func", KeyMatchFunc)

Step 4:在 Model 中使用

和内置函数用法完全一致

1
2
[matchers]
m = r.sub == p.sub && my_func(r.obj, p.obj) && r.act == p.act

RBAC

Overview

核心思想

  1. 传统 ACL(访问控制列表)是用户 → 资源的直接映射,权限多了就爆炸,RBAC 引入角色作为中间层
  2. 用户 → 角色 → 权限(资源 + 操作)
  3. 好处:用户变动时只改角色分配,权限变动时只改角色的权限定义,两者解耦

5 种变体

复杂度递增:从最简单的角色继承,到多租户、条件判断、标准对齐

变体 解决什么问题 典型场景
Basic RBAC 角色继承、用户-角色分配 基础的角色层级(admin 继承 user 的权限)
RBAC with pattern 模式匹配灵活分配角色 路径通配符、正则匹配资源
RBAC with domains 多租户隔离 SaaS 产品中,每个租户有自己的角色体系(A 公司的 admin ≠ B 公司的 admin
RBAC with conditions 条件式权限 “只有在工作时间才能访问”、”只有在同一部门才能操作”
RBAC96 对齐 NIST RBAC96 标准模型 学术合规/标准化需求

关联

  1. Model Syntax 中 [role_definition] 的 g = _, _ → 就是这里说的 Basic RBAC
  2. Constraint Definition 中的 sod、roleMax 等 → 是 RBAC 角色管理的约束保障
  3. Functions 中的 keyMatch 系列 → 用于 RBAC with pattern
  4. Effector → 判定最终 allow/deny 的最后一步

Basic RBAC

角色定义 — [role_definition]

1
2
3
[role_definition]
g = _, _
g2 = _, _
  1. g 是角色系统,_, _ 表示二元关系(用户 → 角色)
  2. g2 是第二个独立的角色系统,用于资源角色(资源 → 角色)
  3. 以只定义 g(用户角色),也可以同时定义 g2(资源也有角色体系)
  4. 两者完全独立,互不干扰

在策略中使用

1
2
p, data2_admin, data2, read     # data2_admin 角色可以读 data2
g, alice, data2_admin # alice 拥有 data2_admin 角色

在 matcher 中使用

1
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

g(r.sub, p.sub) 的含义:请求的 subject(alice)是否直接或间接属于策略中定义的 subject(data2_admin)

问题

先看问题:没有角色时怎么做权限?假设 alice 要读 data2,直接把 alice 写进策略

1
2
3
p, alice, data2, read
p, bob, data2, read
p, charlie, data2, read

问题:100 个用户都要读 data2?写 100 行。有人离职?找到那行删掉。不可维护

引入角色

把”谁能读 data2”从”人“变成”角色“

1
2
p, data2_admin, data2, read    ← 定义:data2_admin 这个角色能读 data2
g, alice, data2_admin ← 分配:alice 属于 data2_admin
  1. 第一行(p 开头):策略规则,说”data2_admin 能读 data2”
  2. 第二行(g 开头):分组规则,说”alice 是 data2_admin”

现在加人只需要一行:

1
2
g, bob, data2_admin      ← bob 也能读了
g, charlie, data2_admin ← charlie 也能读了

删人也简单,删一行 g 规则即可,策略规则完全不用动。

[role_definition] 是什么?
1
2
[role_definition]
g = _, _
  1. 这句话是在告诉 Casbin:我的模型里有一个叫 g 的分组系统,每条规则有两个字段
  2. _, _ 就是”两个参数”的意思(占位符),对应策略文件中 g 开头的行:
1
2
3
g, alice, data2_admin
↑ ↑
第一个 第二个
g2 是什么?
1
2
3
[role_definition]
g = _, _
g2 = _, _
  1. g 管的是用户和角色的关系(用户属于哪个角色)
  2. 有些场景中,资源也有层级
1
2
3
4
文件系统:/docs/engineering/arch/design.doc
↑ 属于 design 目录
↑ 属于 engineering 目录
↑ 属于 docs 目录

你想让 “对 /docs 有读权限 → 对其下所有子目录和文件都有读权限”,就可以用 g2 定义资源的继承关系

1
2
g2, /docs/engineering, /docs
g2, /docs/engineering/arch, /docs/engineering

g 和 g2 互相独立,各管各的

matcher 中的 g() 是怎么工作的
1
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

当请求进来时:

1
e.Enforce("alice", "data2", "read")

Casbin 把请求代入 matcher

  1. r.sub = “alice”,r.obj = “data2”,r.act = “read”
  2. 遍历所有 p 策略,假设检查到 p, data2_admin, data2, read

matcher 变成

1
2
3
g("alice", "data2_admin") && "data2" == "data2" && "read" == "read"
↑ ↑ ↑
要检查这个 直接比较,✓ 直接比较,✓
g(“alice”, “data2_admin”) 做了什么?

它去查所有 g 规则

1
g, alice, data2_admin   ← 找到了!alice 确实属于 data2_admin
  1. 所以 g(“alice”, “data2_admin”) → true
  2. 三个条件都 true → 最终 allow
如果有继承呢?

假设还有:

1
g, data2_admin, super_admin    ← data2_admin 是 super_admin 的子角色

现在请求:

1
e.Enforce("alice", "some_resource", "some_action")

策略是:

1
p, super_admin, some_resource, some_action

matcher 检查 **g(“alice”, “super_admin”)**:

1
2
3
查 g 规则:
alice → data2_admin ← alice 不是 super_admin,继续
data2_admin → super_admin ← 但 alice 的角色 data2_admin 是 super_admin 的子角色

传递推导:alice → data2_admin → super_admin ✓

所以 g(“alice”, “super_admin”) → true。这就是”间接属于“的含义

总结
  1. p 规则定义角色能做什么
  2. g 规则定义谁属于哪个角色(以及角色之间的继承)
  3. matcher 中的 g() 函数负责查 g 规则表,判断请求者是否属于策略中的角色
  4. g2 是可选的第二套独立分组系统,用于资源层级等场景

核心流程

流程 描述
Model 定义规则 声明请求参数、策略结构、角色系统、匹配表达式、效果组合
Policy 提供数据 p 规则定义角色权限,g 规则定义角色分配和继承
Enforce 执行判定 遍历策略 → matcher 匹配 → g() 查角色表 → effector 合并结果
g() 支持传递 直接匹配或通过继承链间接匹配都返回 true

三个重要注意事项

Casbin 不做身份验证

  1. Casbin 只存储和评估用户-角色映射,不验证用户或角色是否存在
  2. Casbin 只管**”授权”(Authorization)**,不管“认证”(Authentication)
  3. 用户是否存在、角色是否合法,是业务系统的事

不要重名

  1. 不要让用户名和角色名相同(如用户 alice 和角色 alice),因为 Casbin 无法区分
  2. Casbin 中一切都是字符串,用户 alice 和角色 alice 是同一个东西,需要区分时用前缀,如 role_alice

继承是传递的

  1. 如果 A 有角色 B,B 有角色 C,那么 A 自动拥有 C
  2. 无层级限制,链条可以无限延伸
1
2
3
g, alice, editor          # alice → editor
g, editor, admin # editor → admin
# 结果:alice 自动拥有 admin 的所有权限

Token 名称约定

策略中 subject 通常放在第一个位置、命名为 sub

1
2
[policy_definition]
p = sub, obj, act

但可以自定义顺序和名称

1
2
[policy_definition]
p = obj, act, subject # subject 放到了第三个位置

这时必须告诉 Casbin subject 在哪一列

1
2
e.SetFieldIndex("p", constant.SubjectIndex, 2)  // 索引从 0 开始,第 3 列是 2
e.DeleteUser("alice") // 现在 Casbin 知道去第 3 列找用户了

否则 DeleteUser() 等 API 会操作错误的列

角色层级(继承)

Casbin 实现的是 RBAC1 风格的层级继承

  1. alice → editor → admin → superadmin
  2. alice 自动继承 editor、admin、superadmin 的所有权限

层级深度限制

  1. 默认最多 10 层继承
  2. 这是为了防止循环继承导致无限递归,也可以根据业务需要调整
1
2
3
func NewRoleManager(maxHierarchyLevel int) rbac.RoleManager {
rm.maxHierarchyLevel = maxHierarchyLevel
}

区分用户和角色

Casbin 中用户和角色都是字符串,没有类型上的区分

扁平 RBAC(无继承)
  1. GetAllSubjects() 返回 g 规则左边的(用户)
  2. GetAllRoles() 返回 g 规则右边的(角色)
有层级继承时

同一个名字可能既是用户又是角色

1
2
g, alice, editor       # editor 是 alice 的角色
g, editor, admin # editor 又是 admin 的"用户"
  1. 这里 editor 既是一个角色(对 alice 而言),又是一个用户(对 admin 而言)
  2. 建议:用命名约定区分,如 role::editor,在业务层判断时检查前缀

显式 vs 隐式角色/权限

alice → editor → admin → superadmin

API 返回内容 示例
GetRolesForUser(“alice”) 直接分配的角色 [editor]
GetImplicitRolesForUser(“alice”) 直接 + 继承的所有角色 [editor, admin, superadmin]
GetPermissionsForUser(“editor”) 直接分配的权限 editor 自身的策略
GetImplicitPermissionsForUser(“alice”) 直接 + 继承的所有权限 alice + editor + admin + superadmin 的策略

关键区别

  1. 显式(不带 Implicit):只看直接分配的那一层
  2. 隐式(带 Implicit):沿继承链往上收集所有层级

实际业务中通常用 Implicit 版本做权限检查,因为你需要知道用户”最终拥有哪些权限“

总结

Model (model.conf)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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 (policy.csv)

1
2
3
4
5
6
7
8
p, admin, data1, read   # admin 角色对 data1 的读权限
p, admin, data2, read # admin 角色对 data2 的读权限
p, editor, data2, write # editor 角色对 data2 的写权限

g, alice, editor # alice 属于 editor
g, bob, admin # bob 属于 admin
g, charlie, editor # charlie 属于 editor
g, editor, admin # editor 继承 admin(关键:使 alice 和 charlie 间接拥有 admin 的权限)

用户、角色、权限三层的映射关系(g 规则 + p 规则)

alice 沿继承链获得的所有权限(直接 + 隐式)

一次 Enforce 请求的完整判定流程

RBAC with Pattern

RBAC + 模式匹配 - 解决的是”角色分配规则太多“的问题

基础 RBAC + 给 g 规则的参数加上通配符/正则能力,让一条分组规则能匹配无限多个实体,从”N 条规则描述 N 个资源“变成”1 条规则描述一类资源“

先看问题

  1. 假设 alice 能读所有书,用基础 RBAC 你要这样写
  2. 每多一本书,就多一行 g 规则,无法接受
1
2
3
4
5
6
7
p, alice, book_group, read

g, /book/1, book_group
g, /book/2, book_group
g, /book/3, book_group
...
g, /book/10000, book_group

用模式匹配解决

把每本书的具体路径替换成模式,一行搞定

1
2
3
p, alice, book_group, read

g, /book/:id, book_group ← 只需这一行

注册匹配函数后,Casbin 就知道 /book/:id 能匹配 /book/1、/book/2、/book/999 等所有路径

怎么注册?

取决于你用不用 domain(多租户):

场景 API 作用
无 domain AddNamedMatchingFunc(“g”, …) 给 g 的参数注册匹配函数
有 domain AddNamedDomainMatchingFunc(“g”, …) 给 g 的 domain 参数注册匹配函数
两者都要 两个都调用 用户-角色和 domain 都支持模式

代码示例

1
2
3
4
5
6
7
e, _ := NewEnforcer("./model.conf", "./policy.csv")

// 无 domain:注册 g 的匹配函数
e.AddNamedMatchingFunc("g", "KeyMatch2", util.KeyMatch2)

// 有 domain:注册 g 的 domain 匹配函数
e.AddNamedDomainMatchingFunc("g", "KeyMatch2", util.KeyMatch2)

原理

之前基础 RBAC 中 g(a, b) 是精确匹配

1
g("alice", "editor")  → 查表:g, alice, editor → 完全相等 → true

注册匹配函数后,g(a, b) 变成模式匹配

1
2
3
g("/book/1", "/book/:id")  → 用 KeyMatch2 判断 "/book/1" 是否匹配 "/book/:id" → true
g("/book/2", "/book/:id") → true
g("/article/1", "/book/:id") → false

匹配函数就是之前 Functions 文档中介绍的那些(keyMatch、keyMatch2、regexMatch 等),只是用在了 g 规则的参数上

完整流程对比

基础 RBAC:

1
2
3
4
p 规则: alice 能读 book_group
g 规则: /book/1 属于 book_group ← 精确匹配
g 规则: /book/2 属于 book_group ← 精确匹配
g 规则: /book/3 属于 book_group ← 精确匹配

RBAC with Pattern

1
2
p 规则: alice 能读 book_group
g 规则: /book/:id 属于 book_group ← 模式匹配,一行顶万行

总结

Model (model.conf)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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) && g(r.obj, p.obj) && r.act == p.act

Policy (policy.csv)

1
2
3
4
5
6
7
8
9
10
p, reader, book_group, read         # reader 角色对 book_group 的读权限
p, librarian, book_group, read
p, librarian, book_group, write # librarian 角色对 book_group 的写权限
p, librarian, journal_group, read # librarian 角色对 journal_group 的读权限

g, alice, reader # alice → reader(用户→角色)
g, bob, librarian # bob → librarian(用户→角色)

g, /book/:id, book_group # 所有 /book/* → book_group(资源→资源组,模式匹配)
g, /journal/:id, journal_group # 所有 /journal/* → journal_group(资源→资源组,模式匹配)

注册匹配函数

1
2
e, _ := NewEnforcer("model.conf", "policy.csv")
e.AddNamedMatchingFunc("g", "KeyMatch2", util.KeyMatch2)

Matcher 中两个 g() 各管各的

1
2
3
g(r.sub, p.sub)  → 用户→角色  → g("alice", "reader")    精确查表
g(r.obj, p.obj) → 资源→资源组 → g("/book/42", "book_group") 模式匹配 /book/:id
r.act == p.act → 动作精确比较

请求 Enforce(“alice”, “/book/42”, “read”) 的判定:

1
2
3
4
5
1. 遍历 p 策略 → 命中 p, reader, book_group, read
2. g("alice", "reader") → g 规则: alice → reader ✓
3. g("/book/42", "book_group") → g 规则: /book/:id → book_group, KeyMatch2 匹配 ✓
4. "read" == "read" → ✓
5. 三个条件全 true → ALLOW

完整的权限结构 — 用户 → 角色 → 权限,加上资源分组(绿色)用 :id 模式一条规则覆盖所有书籍

alice 请求读 /book/42 的完整判定流程,橙色节点是 KeyMatch2 模式匹配的关键步骤

基础 RBAC vs RBAC with Pattern 的对比,N 条 → 1 条

RBAC with Domains

核心概念

  1. 之前的 RBAC 模型中,g = _, _ 定义的是全局角色 - 一个用户在整个系统中只有一种角色身份
  2. 但在多租户/多云场景下,同一个用户在不同租户中可能拥有不同角色
Key Value
普通 RBAC alice → admin(全局唯一角色)
域 RBAC alice → admin in tenant1, alice → user in tenant2(角色随域变化)

角色定义:三元组

1
2
[role_definition]
g = _, _, _
  1. 第 1 个 _:主体(用户)
  2. 第 2 个 _:角色
  3. 第 3 个 _:域(Domain) - 租户、工作空间、组织等

核心区别

  1. g = _, _ 是二元(用户→角色)
  2. g = _, _, _ 是三元(用户→角色在某域中)

策略示例解析

1
2
3
4
5
p, admin, tenant1, data1, read
p, admin, tenant2, data2, read

g, alice, admin, tenant1
g, alice, user, tenant2

逐行解读

行 类型 含义
p, admin, tenant1, data1, read 策略 admin 角色在 tenant1 域中可以 read data1
p, admin, tenant2, data2, read 策略 admin 角色在 tenant2 域中可以 read data2
g, alice, admin, tenant1 绑定 alice 在 tenant1 中是 admin
g, alice, user, tenant2 绑定 alice 在 tenant2 中是 user

Enforce 推演

请求 结果 原因
Enforce(“alice”, “tenant1”, “data1”, “read”) Allow alice 在 tenant1 是 admin,admin 在 tenant1 可读 data1
Enforce(“alice”, “tenant2”, “data1”, “read”) Deny alice 在 tenant2 是 user,不是 admin
Enforce(“alice”, “tenant2”, “data2”, “read”) Deny alice 在 tenant2 是 user,user 没有任何策略
Enforce(“bob”, “tenant2”, “data2”, “read”) Deny bob 没有任何 g 绑定

关键:alice 虽然全局是 admin(在 tenant1),但她在 tenant2 只是 user,角色不会跨域传播

Matcher 变化

1
2
[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act

对比基础 RBAC 的 matcher

Key Value
基础 RBAC m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
域 RBAC m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act

多了两部分:

  1. g(r.sub, p.sub, r.dom) - 带域的角色继承查询,检查用户是否在该域中拥有该角色
  2. r.dom == p.dom - 请求的域必须匹配策略的域

自定义域字段名

域不一定要叫 dom,可以是 tenant、workspace、org 等任意名称:

1
2
3
4
5
6
7
8
9
10
11
[request_definition]
r = sub, tenant, obj, act

[policy_definition]
p = sub, tenant, obj, act

[role_definition]
g = _, _, _

[matchers]
m = g(r.sub, p.sub, r.tenant) && r.tenant == p.tenant && r.obj == p.obj && r.act == p.act
  1. 只是把 dom 换成了 tenant,逻辑完全一样,Casbin 并不关心字段名叫什么,只关心位置和匹配关系
  2. 如果需要在域字段上使用模式匹配(如 keyMatch),只需在 matcher 中**替换相等判断**
1
m = g(r.sub, p.sub, r.tenant) && keyMatch(r.tenant, p.tenant) && r.obj == p.obj && r.act == p.act

这样 p.tenant = "org_*" 就能匹配 r.tenant = "org_engineering"

域字段位置自定义

默认约定域在第 2 个位置(sub, dom, obj, act,index=1),如果放在其他位置

1
2
[policy_definition]
p = sub, obj, act, domain # domain 在第 4 位,index=3

需要在代码中设置索引:

1
e.SetFieldIndex("p", constant.DomainIndex, 3)

否则 GetAllUsersByDomain("domain1") 这类域相关 API 会报错,因为 Casbin 不知道域字段在哪

总结

RBAC with Conditions

核心思想

  1. 之前所有 RBAC 模型中,g 规则一旦写入就永久生效
  2. Conditional RoleManager 引入了一个关键变化:角色绑定可以附带条件,条件不满足时角色绑定失效
  3. 最典型的场景:时间限定角色 - 某人在某个时间段内临时担任 admin,过期自动失效

角色定义语法变化

1
2
[role_definition]
g = _, _, (_, _)

对比

模型 定义 含义
基础 RBAC g = _, _ 用户→角色,无条件
域 RBAC g = _, _, _ 用户→角色→域,无条件
条件 RBAC g = _, _, (_, _) 用户→角色,附带 2 个条件参数
条件+域 RBAC g = _, _, _, (_, _) 用户→角色→域,附带 2 个条件参数

(_, _) 中的每个 _ 代表一个额外的参数位,在 g 规则中以实际值填充(如起止时间),运行时传给条件函数判断

策略解析

1
2
3
4
5
6
7
g, alice, data2_admin, 0000-01-01 00:00:00, 0000-01-02 00:00:00
g, alice, data3_admin, 0000-01-01 00:00:00, 9999-12-30 00:00:00
g, alice, data4_admin, _, _
g, alice, data5_admin, _, 9999-12-30 00:00:00
g, alice, data6_admin, _, 0000-01-02 00:00:00
g, alice, data7_admin, 0000-01-01 00:00:00, _
g, alice, data8_admin, 9999-12-30 00:00:00, _

_ 表示忽略该参数(不做限制),逐行分析

g 规则 条件参数 含义 时间匹配结果
alice → data2_admin 0000-01-01 ~ 0000-01-02 只在这 1 天内有效 false(已过期)
alice → data3_admi 0000-01-01 ~ 9999-12-30 几乎永久有效 true
alice → data4_admi _, _ 无条件限制(等同普通 RBAC) true
alice → data5_admi _ ~ 9999-12-30 无开始限制,永不过期 true
alice → data6_admi _ ~ 0000-01-02 无开始限制,但已过期 false
alice → data7_admi 0000-01-01 ~ _ 有开始时间,无结束限制 true
alice → data8_admi 9999-12-30 ~ _ 开始时间在遥远未来 false

Enforce 推演

请求 alice 的角色绑定 条件函数 结果
(“alice”, “data1”, “read”) data1 无需角色(p 直接给 alice) - true
(“alice”, “data2”, “write”) alice → data2_admin 时间已过 → false false
(“alice”, “data3”, “read”) alice → data3_admin 永久有效 → true true
(“alice”, “data4”, “write”) alice → data4_admin 无条件 → true true
(“alice”, “data5”, “read”) alice → data5_admin 永不过期 → true true
(“alice”, “data6”, “write”) alice → data6_admin 已过期 → false false
(“alice”, “data7”, “read”) alice → data7_admin 无结束限制 → true true
(“alice”, “data8”, “write”) alice → data8_admin 未到开始时间 → false false

关键点:条件函数作用在 g 规则(角色绑定) 上,不是作用在 p 规则(权限策略) 上,**角色绑定失效 = 该角色完全不生效**

运行机制

与 无条件的 data3 对比

代码注册方式

注意:每条 g 规则需要单独注册条件函数。也可以用 AddNamedMatchingFunc 为所有 g 规则统一注册(但粒度不同)

1
2
3
4
// 为每条 g 规则绑定条件函数
e.AddNamedLinkConditionFunc("g", "alice", "data2_admin", util.TimeMatchFunc)
e.AddNamedLinkConditionFunc("g", "alice", "data3_admin", util.TimeMatchFunc)
// ...

自定义条件函数

  1. 签名:func(args ...string) (bool, error)
  2. 可以自定义任意条件逻辑,不只限于时间:
1
2
3
4
5
6
// 示例:IP 范围条件
func IPRangeMatchFunc(args ...string) (bool, error) {
clientIP := args[0] // 运行时传入
allowedIP := args[1] // 策略中配置
return ipMatch(clientIP, allowedIP), nil
}

条件+域的组合

1
2
[role_definition]
g = _, _, _, (_, _)

策略格式变为 5 列:g, user, role, domain, condArg1, condArg2

1
g, alice, data2_admin, domain2, 0000-01-01 00:00:00, 0000-01-02 00:00:00

注册使用 AddNamedDomainLinkConditionFunc(多了 domain 参数):

1
e.AddNamedDomainLinkConditionFunc("g", "alice", "data2_admin", "domain2", util.TimeMatchFunc)

三种匹配函数的区别

函数类型 作用阶段 注册方式 控制什么
MatchingFunc g 查找时 AddNamedMatchingFunc 主体的模式匹配(如 u:* 匹配所有用户)
DomainMatchingFunc g 查找时 AddNamedDomainMatchingFunc 域的模式匹配(如 org_* 匹配多个组织)
LinkConditionFunc g 规则判断时 AddNamedLinkConditionFunc 角色绑定是否生效(如时间条件)

三者独立且串联:主体要匹配 → 域要匹配 → 条件要满足,角色绑定才真正生效

RBAC vs. RBAC96

RBAC96 是什么

RBAC96 是 NIST(美国国家标准与技术研究院)在 1996 年提出的 RBAC 标准模型家族,是学术界和工业界的 RBAC 基准,它定义了四个层级

逐级对比

RBAC0 - 基础三要素

RBAC0 定义了 RBAC 的最小可用集合:用户、角色、权限 及其关系

要素 说明
Users 用户
Roles 角色
Permissions 权限(操作+对象)
UA User-Role Assignment(用户→角色绑定)
PA Permission-Role Assignment(权限→角色绑定)

Casbin 完全覆盖:p 规则就是 PA,g 规则就是 UA

RBAC1 — 角色继承

在 RBAC0 基础上加入角色层级(Role Hierarchy):角色可以继承其他角色的权限

1
2
admin 继承 editor 继承 viewer
→ admin 自动拥有 viewer 和 editor 的所有权限

Casbin 完全覆盖:g 规则天然支持传递性继承

1
2
3
g, alice, senior_editor
g, senior_editor, editor
g, editor, viewer

alice → senior_editor → editor → viewer,三级继承,alice 拥有所有权限

RBAC2 - 约束

在 RBAC0 基础上加入约束规则,最典型的是

约束类型 含义 示例
职责分离(SoD) 同一用户不能同时拥有互斥角色 不能既是”出纳”又是”审计”
基数限制 一个用户最多/最少拥有几个角色 每人最多 3 个角色
先决条件 要获得某角色必须先拥有另一角色 要当 admin 必须先是 editor

Casbin 部分覆盖

RBAC2 要求 Casbin 支持情况
职责分离(静态/动态 SoD) 可通过 deny-override policy effect 间接实现,或通过 constraint_definition
基数限制(如”每人最多 3 个角色”) 不支持 - Casbin 没有内置数量约束
先决条件角色 不支持 - 角色分配是扁平的
RBAC3 - 完整组合

RBAC3 = RBAC1 + RBAC2,即角色继承 + 约束,Casbin 同样是部分覆盖,受限于 RBAC2 的约束支持

三个关键差异

差异一:用户 vs 角色 - 类型不区分

RBAC96 中 User 和 Role 是不同的类型实体,Casbin 不做区分 - 都是字符串

1
2
3
p, admin, book, read      # admin 是角色,作为策略主体
p, alice, book, read # alice 是用户,也作为策略主体
g, amber, admin # amber→admin 的角色绑定
  1. GetAllSubjects() 返回所有策略主体:[admin, alice](混合了用户和角色)
  2. GetAllRoles() 只返回 g 规则右侧:[admin]

实践建议:用命名约定区分,如 user::alice、role::admin。有些项目正是这样做的:u:123 表示用户,r:5 表示角色

差异二:权限类型自由

RBAC96 定义了 7 种固定权限类型,Casbin 的权限是任意字符串

1
2
3
4
p, admin, /api/users, GET       # act = GET
p, admin, /api/users, POST # act = POST
p, editor, document, approve # act = approve(自定义操作)
p, viewer, dashboard, view # act = view

配合 regexMatch(r.act, p.act) 还能用正则匹配,如 p.act = “(GET|POST)” 同时匹配两种 Method

差异三:域(Domains)- 超越 RBAC96
  1. RBAC96 没有多租户概念
  2. Casbin 的 g = _, _, _ 提供了域级别的角色隔离,这是对 RBAC96 的扩展

Priority Model

在基础 RBAC 中,effect 是 some(where (p.eft == allow)) - 只要有任何一条 allow 就放行,但现实中有冲突场景

1
2
p, alice, data1, read, allow       # alice 可以读
p, data1_group, data1, read, deny # 但她所在的组被禁止读

alice 同时匹配两条策略,一条 allow、一条 deny,谁说了算?Priority Model 就是来解决这个问题的

隐式优先级(策略顺序)

1
2
[policy_effect]
e = priority(p.eft) || deny

规则:策略在文件/数据库中的顺序就是优先级,**先出现的优先级最高**

1
2
p, alice, data1, write, allow       # 第 1 条,优先级最高 → 生效
p, data1_group, data1, write, deny # 第 2 条,被第 1 条覆盖

缺点:依赖顺序,维护困难,数据库里排序不稳定就会出问题

显式优先级(数字字段)

在 p 定义中加一个 priority 字段,数字越小优先级越高

1
2
[policy_definition]
p = priority, sub, obj, act, eft

完整示例分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 角色级策略(优先级 10)
p, 10, data1_deny_group, data1, read, deny
p, 10, data1_deny_group, data1, write, deny
p, 10, data2_allow_group, data2, read, allow
p, 10, data2_allow_group, data2, write, allow

# 个人级策略(优先级 1,更高)
p, 1, alice, data1, write, allow
p, 1, alice, data1, read, allow
p, 1, bob, data2, read, deny

# 角色绑定
g, bob, data2_allow_group
g, alice, data1_deny_group

逐个请求分析:

请求 匹配的策略 结果 原因
alice, data1, write p,1,alice,data1,write,allow allow 优先级 1 > 10,个人策略赢
bob, data2, read p,1,bob,data2,read,deny (优先级1) vs p,10,data2_allow_group,data2,read,allow (优先级10) deny 个人 deny 优先级更高
bob, data2, write p,10,data2_allow_group,data2,write,allow (优先级10) allow 只有角色 allow,无更高优先级 deny

注意:AddPolicy / AddPolicies 会按优先级排序插入,但 UpdatePolicy 不会重新排序 - 所以不要通过 Update 修改优先级字段的值

自定义优先级字段名

默认字段名是 priority,如果用其他名字(如 customized_priority),需要手动设置索引

1
2
3
// 必须在 LoadPolicy 之前调用
e.SetFieldIndex("p", constant.PriorityIndex, 0) // 第 0 个字段是优先级
e.LoadPolicy()

不设置会怎样? Casbin 无法识别哪个字段是优先级,策略按原始顺序匹配,优先级逻辑失效

主体优先级(角色/用户层级)

核心思想:优先级不来自数字字段,而是来自角色树的深度 - 越靠近叶子节点(用户),优先级越高

1
2
[policy_effect]
e = subjectPriority(p.eft) || deny

角色树:

1
2
3
4
5
6
role: root                    # 自动优先级: 30(最低)
└─ role: admin # 自动优先级: 20
├─ role: editor # 自动优先级: 10
│ └─ user: jane # 隐含优先级: 0(最高)
└─ role: subscriber # 自动优先级: 10
└─ user: alice # 隐含优先级: 0(最高)

规则:叶子(用户)> 内部角色(editor)> 管理员角色(admin)> 根角色(root)

策略分析:jane, data1, read → allow(jane 的优先级 > editor > admin > root)

1
2
3
4
p, root, data1, read, deny       # 优先级 30
p, admin, data1, read, deny # 优先级 20
p, editor, data1, read, deny # 优先级 10
p, jane, data1, read, allow # jane 是叶子,优先级最高

设计直觉

  1. 上层角色定义的是”默认策略“(如 deny),下层用户/角色可以”覆盖“它
  2. 这符合企业场景 - 高层规定默认禁止,但可以给特定人员开绿灯

约束

  1. 角色层级必须是树(不能有环),同一用户的多角色应在同一深度

假设角色树如下

1
2
3
4
5
6
root
├─ admin (depth 1)
│ └─ editor (depth 2)
│ └─ alice
└─ viewer (depth 1)
└─ alice

alice 同时属于 editor(depth 2)和 viewer(depth 1)。如果策略是:

1
2
p, editor, data1, read, allow    # editor 说允许
p, viewer, data1, read, deny # viewer 说禁止
  1. alice 匹配了两条,一条来自 depth 2(editor),一条来自 depth 1(viewer),主体优先级的规则是越深优先级越高,所以 editor 的 allow 赢
  2. 但问题在于:alice 通过 viewer 路径是 depth 1,通过 editor 路径是 depth 2,同一个用户在不同路径下深度不同,Casbin 无法确定 alice 的”真实深度“是多少,可能取到错误的优先级

如果多角色在同一深度

1
2
3
4
5
6
root
└─ admin (depth 1)
├─ editor (depth 2)
│ └─ alice
└─ subscriber (depth 2)
└─ alice

alice 通过 editor 和 subscriber 都是 depth 2,深度一致,不存在歧义,同深度平局时按策略顺序打破

三种优先级模式对比

模式 优先级来源 配置方式 适用场景
隐式优先级 策略顺序 `priority(p.eft)
显式优先级 数字字段 p = priority, sub, obj, act, eft 需要精细控制优先级,如个人策略覆盖角色策略
主体优先级 角色树深度 `subjectPriority(p.eft)

effect 表达式解读

1
e = priority(p.eft) || deny
  1. priority(...) — 按优先级排序所有匹配的策略,取优先级最高的那条的 eft 值
  2. || deny — 如果没有匹配的策略,默认 deny

对比基础 RBAC:

1
2
e = some(where (p.eft == allow))   # 基础 RBAC:有 allow 就放行
e = priority(p.eft) || deny # Priority:优先级最高的策略说了算

Super Administrator

核心机制

在 matcher 末尾加 || r.sub == "root"

1
2
[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act || r.sub == "root"

matcher 是一个布尔表达式,只要整个表达式为 true 就放行

条件 结果
正常策略匹配成功 true → allow
策略没匹配,但 r.sub == “root” true → allow
策略没匹配,也不是 root false → deny

root 用户完全绕过策略检查,不需要任何 p 规则

与 Priority Model 协同

  1. 当 root 发起请求时,|| r.sub == "root" 让所有策略的 matcher 都为 true - 相当于所有 p 规则都”匹配“了
  2. 然后交给 effect 判断
Effect root 的结果 原因
some(where (p.eft == allow)) allow 所有策略都匹配,只要有一条 allow 就放行
`priority(p.eft) deny`
  1. Priority Model 下,如果存在一条 p, 1, *, *, deny(高优先级 deny),root 也会被拒绝
  2. || r.sub == "root" 在 matcher 里确保了”匹配“,但 effect 取的是优先级最高的策略的 eft,root 并没有获得特殊豁免