RBAC 与多租户
RBAC 是 Casbin 使用率最高的模型。本章覆盖角色继承机制、多租户(domains)模型、完整的 RBAC API,以及角色通配符匹配等进阶用法。
角色继承机制回顾
p, admin, data, write # 权限授予角色
g, alice, admin # 用户绑定角色g, A, B读作 “A 继承 B 的权限"(A 可以是用户也可以是角色)- 继承是传递的:
g, alice, admin+g, admin, root⇒ alice 拥有 root 的所有权限 - 一个用户可以有多个角色,一个角色可以有多个父角色
🔬 深入原理:Casbin 内部用 RoleManager 维护一张角色继承图(有向图),g(r.sub, p.sub) 的每次调用是一次图可达性查询。默认实现限制最大继承层级为 10,防止环形继承导致死循环。策略加载时构图,Enforce 时只查图,因此角色关系再复杂也不拖慢判定。
// 调整最大继承层级(极少需要)
import "github.com/casbin/casbin/v3/rbac/default-role-manager"
e.SetRoleManager(defaultrolemanager.NewRoleManagerImpl(20))RBAC API:用户与角色
以下 API 均为 Enforcer 的方法。修改类 API 会同步更新内存和 adapter(若开启 AutoSave)。
| API | 签名要点 | 说明 |
|---|---|---|
AddRoleForUser(user, role) |
(bool, error) |
绑定角色;已存在返回 false |
AddRolesForUser(user, roles) |
一次绑定多个角色 | |
DeleteRoleForUser(user, role) |
解绑单个角色 | |
DeleteRolesForUser(user) |
解绑该用户所有角色 | |
DeleteUser(user) |
删除用户(其角色绑定 + 直接权限) | |
DeleteRole(role) |
删除角色(相关绑定 + 角色的权限) | |
GetRolesForUser(user) |
([]string, error) |
用户的直接角色 |
GetUsersForRole(role) |
拥有该角色的用户 | |
HasRoleForUser(user, role) |
是否有某直接角色 |
e.AddRoleForUser("alice", "admin")
e.AddRolesForUser("bob", []string{"editor", "viewer"})
roles, _ := e.GetRolesForUser("alice") // ["admin"]
users, _ := e.GetUsersForRole("admin") // ["alice"]
e.DeleteRoleForUser("bob", "viewer")
e.DeleteUser("alice") // 用户离职:一键清理RBAC API:权限
“权限"即以用户/角色为 sub 的 p 策略。
| API | 说明 |
|---|---|
AddPermissionForUser(user, perm...) |
给用户/角色直接加权限(等价于 AddPolicy) |
DeletePermissionForUser(user, perm...) |
删除某条权限 |
DeletePermissionsForUser(user) |
删除该用户/角色的全部权限 |
GetPermissionsForUser(user) |
用户/角色的直接权限 |
HasPermissionForUser(user, perm...) |
是否有某直接权限 |
// "权限"就是策略的语法糖:下面两句等价
e.AddPermissionForUser("editor", "/api/articles/*", "POST")
e.AddPolicy("editor", "/api/articles/*", "POST")
perms, _ := e.GetPermissionsForUser("editor")
// [["editor", "/api/articles/*", "POST"]]隐式(Implicit)API:穿透角色继承查询
GetRolesForUser 只返回直接角色,GetPermissionsForUser 只返回直接权限。要查"继承链上所有的”,用 Implicit 系列 —— 做"我的权限"页面必用:
| API | 直接版 | 区别 |
|---|---|---|
GetImplicitRolesForUser(user) |
GetRolesForUser |
含间接继承的角色 |
GetImplicitPermissionsForUser(user) |
GetPermissionsForUser |
含所有角色带来的权限 |
GetImplicitUsersForPermission(perm...) |
— | 拥有某权限的所有用户(穿透角色) |
p, admin, data, write
g, alice, admin
g, admin, superadmin
p, superadmin, sys, managee.GetRolesForUser("alice") // ["admin"] 只有直接角色
e.GetImplicitRolesForUser("alice") // ["admin", "superadmin"]
e.GetPermissionsForUser("alice") // [] alice 没有直接权限
e.GetImplicitPermissionsForUser("alice") // [[admin data write] [superadmin sys manage]]RBAC with domains(多租户)
SaaS 场景:alice 在租户 A 是管理员,在租户 B 只是访客。加一个 domain(域) 维度:
model.conf
[request_definition]
r = sub, dom, obj, act
[policy_definition]
p = sub, dom, obj, act
[role_definition]
g = _, _, _ # (用户, 角色, 域):角色绑定按域隔离
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.actpolicy.csv
p, admin, tenant1, data1, read
p, admin, tenant1, data1, write
p, admin, tenant2, data2, read
g, alice, admin, tenant1 # alice 只在 tenant1 是 admin
g, bob, admin, tenant2代码
e.Enforce("alice", "tenant1", "data1", "write") // true
e.Enforce("alice", "tenant2", "data2", "read") // false:tenant2 里 alice 无角色🚨 陷阱:matcher 里 g(r.sub, p.sub, r.dom) 之外还要写 r.dom == p.dom。前者只保证"在该域内有此角色”,后者保证"策略也是该域的"。漏掉后者时,只要用户在任意域有 admin 角色,就能用上所有域的 admin 策略——典型的越权漏洞。
域专用 API
| API | 说明 |
|---|---|
AddRoleForUserInDomain(user, role, domain) |
在某域内绑定角色 |
DeleteRoleForUserInDomain(user, role, domain) |
在某域内解绑 |
GetRolesForUserInDomain(user, domain) |
某域内的角色 |
GetUsersForRoleInDomain(role, domain) |
某域内某角色的用户 |
GetPermissionsForUserInDomain(user, domain) |
某域内的权限 |
GetAllDomains() |
所有出现过的域 |
GetDomainsForUser(user) |
用户有角色的所有域 |
DeleteDomains(domains...) |
删除整个域的所有策略(租户注销) |
e.AddRoleForUserInDomain("carol", "viewer", "tenant1")
domains, _ := e.GetDomainsForUser("alice") // ["tenant1"]
e.DeleteDomains("tenant_old") // 清空旧租户全部数据💡 最佳实践:租户 ID 建议加前缀(如 tenant:123),与用户 ID(user:456)、角色名(role:admin)区分命名空间,避免"某用户名恰好等于某角色名"造成的意外继承。这一约定对所有 Casbin 项目都适用。
角色/域的通配符匹配(AddNamedMatchingFunc)
默认 g 关系是精确字符串匹配。通过给 RoleManager 注入匹配函数,g 的参数也能用通配符:
资源角色用通配符
import "github.com/casbin/casbin/v3/util"
// 让 g 关系支持 keyMatch2 模式匹配
e.AddNamedMatchingFunc("g", "keyMatch2", util.KeyMatch2)p, book_group, book_group, read
g2, /books/:id, book_group # 所有 /books/xxx 都属于 book_group配合 g2(r.obj, p.obj),/books/42 自动归入 book_group。
域用通配符:全域管理员
e.AddNamedDomainMatchingFunc("g", "keyMatch2", util.KeyMatch2)g, alice, admin, * # alice 在所有域都是 admin
p, admin, domain1, data1, reade.Enforce("alice", "domain1", "data1", "read") // true:* 匹配 domain1⚡ 性能提示:开启 MatchingFunc 后,角色图查询从哈希查找退化为逐条模式匹配,角色关系很多时有明显开销。只在确实需要通配的 g 关系上开启,且模式条目尽量少。
RBAC 数据建模建议
一个典型后台系统的三层结构:
用户(user:1001) ──g──→ 角色(role:editor) ──p──→ 资源+操作(/api/articles/*, POST)| 层 | 存在哪 | 谁维护 |
|---|---|---|
| p 行(角色→权限) | Casbin 策略 | 开发/运维,随功能上线变更 |
| g 行(用户→角色) | Casbin 策略 | 管理后台,运营随时改 |
| 用户信息、角色显示名 | 业务数据库 | 业务系统 |
💡 最佳实践:Casbin 里只存 ID 和权限关系,角色的中文名、描述、图标等展示信息放业务表,两边用角色 key(如 role:editor)关联。不要试图把业务字段塞进策略。
常见陷阱
🚨 GetRolesForUser 拿不到间接角色:“我的角色"显示不全、权限计算漏配,多半是该用 GetImplicitRolesForUser / GetImplicitPermissionsForUser 的地方用了直接版。
🚨 用户名与角色名撞名:g, alice, admin 中 Casbin 并不区分哪个是"用户"哪个是"角色”——它们都只是字符串节点。若有个用户恰好叫 admin,g, admin_user_x, admin 会让此人继承用户 admin 的直接权限。用 user: / role: 前缀隔离命名空间。
🚨 domain 模型漏写 r.dom == p.dom:跨租户越权,见上文。写完 domain 模型必须用"用户在 A 域有角色,访问 B 域资源"的用例做负向测试。
🚨 删除角色只删了 g 行:手动 RemoveGroupingPolicy 只解绑用户,角色的 p 行还留着,成为"幽灵权限"。删角色用 DeleteRole(role),删用户用 DeleteUser(user),让 Casbin 一并清理。
🚨 环形继承:g, a, b + g, b, a 不会报错,但依赖最大层级(10)兜底,行为不直观。管理后台在写入 g 关系前应做环检测。
🚨 MatchingFunc 全局生效:AddNamedMatchingFunc 作用于整个 g 关系的所有条目,不能只对某几行开启。模式行与精确行混用时注意 * 等字符出现在普通角色名里的误匹配。