直接复用Casbin默认RBAC模型会因缺失租户维度导致越权风险,必须通过扩展请求定义为四元组、策略显式绑定tenant_id、按租户缓存Enforcer并定制Adapter来实现安全隔离。

为什么直接复用 Casbin 默认模型会踩坑
Casbin 默认的 RBAC 模型(如 rbac_model.conf)不带租户维度,所有角色、策略、资源都在同一命名空间。一旦多个租户共用一个 Enforcer 实例,alice 在租户 A 的 admin 角色可能意外获得租户 B 的数据权限——这不是逻辑错误,而是模型设计缺失导致的越权风险。
根本问题在于:Casbin 的策略存储(如 file、mysql、redis)默认不强制隔离,而它的 enforce() 方法也只接收 sub, obj, act 三个参数,没有原生租户上下文。
- 必须把租户标识(如
tenant_id)作为策略的一部分嵌入到sub或obj中,或扩展为四元组 - 不能靠启动多个
Enforcer实例来“隔离”——内存和连接开销大,且无法共享用户/角色定义 - 使用
FilteredAdapter时要注意:过滤器只影响策略加载,不改变运行时匹配逻辑
用自定义请求定义 + 命名空间策略实现租户感知
最轻量且兼容性最好的方式是升级请求定义,把租户 ID 作为第四个参数传入 enforce(),并修改模型文件适配:
#[request_definition] r = sub, tenant, obj, act [policy_definition] p = sub, tenant, obj, act [role_definition] g = _, _, _ [policy_effect] e = some(where (p.eft == allow)) [matchers] m = r.sub == p.sub && r.tenant == p.tenant && r.obj == p.obj && r.act == p.act
这样每条策略都显式绑定租户,例如:p, admin, tenant-a, /api/users, read 不会匹配 r, alice, tenant-b, /api/users, read。
- 调用时必须传四参数:
e.Enforce("alice", "tenant-a", "/api/users", "read") - 角色继承也要带租户:
g, alice, admin, tenant-a;跨租户继承(如平台级超级管理员)需额外建模,不推荐 - 如果已有大量双参数调用,可封装一层适配函数,避免全量改业务代码
如何安全复用用户与角色定义而不泄露权限
多租户系统中,用户账号常全局唯一(如邮箱),但角色权限必须分租户生效。直接在策略里重复写 user@domain.com + tenant-id 容易冗余且难维护。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
推荐做法是:用户和角色定义仍存全局表(如 users、roles),但策略表(casbin_rule)强制包含 tenant_id 字段,并在 Adapter 层拦截写入/查询。
- 使用
gorm-adapter时,继承GormAdapter并重写LoadPolicy()和SavePolicy(),自动添加WHERE tenant_id = ?条件 - 不要在
sub字段拼接tenant:user_id—— 这会让GetUsersForRole()等内置方法失效 - 若需跨租户审计,保留原始策略快照,但运行时 enforce 永远走租户过滤后的子集
注意 Enforcer 初始化与缓存失效的边界
每个 HTTP 请求都新建 Enforcer 实例?太重。全局单例?又怕策略更新不及时。真实场景下,租户策略变更频率低(如后台配置),但读多写少。
- 建议按租户缓存
*casbin.Enforcer实例(如用sync.Map[string]*casbin.Enforcer),key 为tenant_id - 当租户策略更新时(如 RBAC 配置保存),显式调用对应
enforcer.LoadPolicy(),而非全量 reload - 别依赖
AutoLoadPolicy()—— 它基于文件修改时间或轮询,对数据库 adapter 不可靠,且无法感知租户粒度变更 - 如果用了 Redis adapter,确保 key 命名含租户前缀,例如
casbin:policy:tenant-a
最难绷的是:开发者常把「租户隔离」当成配置开关,实际它渗透在模型定义、策略存储、运行时调用、缓存策略四个层面——漏掉任一环,隔离就形同虚设。










