Skip to content
Go
RBAC 与多租户

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, manage
e.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.act

policy.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, read
e.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 并不区分哪个是"用户"哪个是"角色”——它们都只是字符串节点。若有个用户恰好叫 adming, 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 关系的所有条目,不能只对某几行开启。模式行与精确行混用时注意 * 等字符出现在普通角色名里的误匹配。