在 SaaS 应用里,权限设计难点往往不只是“谁能做什么”,而是“谁在自己的租户里能做什么”。如果系统既要复用统一平台能力,又要保证商户、企业或团队之间的数据和权限边界清晰,多租户权限模型就必须提前设计好。本文用 PHP-Casbin 的域模型为例,从多商户电商场景出发,说明模型结构、策略配置和代码判定方式,帮助你判断它是否适合当前项目的权限架构。
为什么 SaaS 权限系统要引入多租户模型
多租户(multi-tenancy)是一种软件架构方式,目标是在同一套系统或组件之上服务多个租户,同时保证不同用户之间的数据隔离。在不少中文语境里,它也会被称作域租户。
对 SaaS、PaaS 这类云服务来说,这种设计尤其常见。多个客户共享平台能力,但每个客户都需要独立的组织结构、角色体系和权限策略。如果仍然使用只有全局角色的普通 RBAC,很容易遇到一个问题:同一个用户在不同租户里的身份并不相同,权限也不应互相串联。
PHP-Casbin 在提供全局 RBAC 的同时,也支持带特定域的权限模型。这样一来,用户在不同租户或域中,可以拥有不同的角色,并根据各自租户内的策略完成授权判定。
这类能力在大型项目里很实用,尤其是面向企业的 SaaS 平台。租户之间既共享系统资源,又保留各自独立的权限控制边界,架构上会更清晰。
用多商户电商场景看权限边界
可以把多租户场景理解成一个多商户电商平台。这里的平台商户就是租户:每个商户有自己的管理员、角色分配方式和权限定义,同时商品、人员和业务数据在逻辑上彼此隔离,但仍共享同一个电商平台。

多商户电商平台
商户1,用户1,属于管理员角色
商品1
商户2,用户2,属于管理员角色
商品2
在这个例子里,商户 1 和商户 2 都运行在同一平台之上,但权限判断不能跨商户混用。也就是说:
- 商户 1 下的用户和商品,只能在商户 1 的权限体系里判断;
- 商户 2 下的用户和商品,也只能在商户 2 的规则里判断;
- 即使两个商户里都存在名为管理员的角色,它们也应当属于不同租户上下文。
这正是多租户权限模型要解决的问题:同一套角色命名可以复用,但作用范围必须被租户边界明确限制住。
PHP-Casbin 的域模型是怎么表达租户的
官方仓库已经提供了多租户模型配置,文件名为 rbac_with_domains_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
和普通 RBAC 模型相比,这里多了一个关键参数 dom。它代表的就是租户,也就是上文中的商户、域或 tenant。
请求与策略为什么都要带上 dom
在 [request_definition] 里,访问请求被定义为 sub, dom, obj, act,也就是“谁、在哪个租户、访问什么对象、执行什么动作”。
在 [policy_definition] 里,策略也同样包含 sub, dom, obj, act。这意味着权限规则不是单纯写给某个角色,而是写给“某个角色在某个租户中的权限”。
g = _, _, _ 代表什么
在 [role_definition] 中,g = _, _, _ 有三个占位符,分别可以理解为:用户、角色、租户/域。

也就是说,角色继承关系不再是简单的“用户属于角色”,而是“用户在某个租户里属于某个角色”。
matchers 如何保证租户隔离
核心判断逻辑在 [matchers] 中:
m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
这里的 g(r.sub, p.sub, r.dom) 会判断用户、角色和租户三者之间的关系;而 r.dom == p.dom 则进一步要求请求所在租户必须和策略中的租户完全一致。这样即使角色名相同,也不会跨租户生效。
策略文件怎么写,才能做到租户内授权、租户间隔离
对应前面的电商场景,可以分别为商户 1(tenant1)和商户 2(tenant2)定义管理员权限,再把 user1、user2 绑定到各自租户下的 admin 角色:
p, admin, tenant1, goods1, read
p, admin, tenant1, goods1, write
p, admin, tenant2, goods2, read
p, admin, tenant2, goods2, write
g, user1, admin, tenant1
g, user2, admin, tenant2
这份配置放在 rbac_with_domains_policy.csv 文件中。
这里可以直接看出几个设计要点:
p规则描述的是角色在特定租户中的资源权限;g规则描述的是用户在特定租户中的角色归属;- 同样是
admin,在tenant1和tenant2中其实是两套独立的授权上下文; - 资源对象
goods1、goods2也天然落在各自租户边界内。
这种写法的优势是直观。你不需要为每个租户额外造一套全新角色命名,只要把租户维度写进模型和策略,Casbin 在判定时就会自动把边界卡住。
在 PHP 里如何初始化 Enforcer 并做权限判定
如果项目里还没有引入 Casbin 依赖,可以先安装:
composer require casbin/casbin
下面演示的是原生 PHP 代码。如果你使用的是 Laravel、ThinkPHP、Yii 等主流框架,通常可以直接接入各自扩展,再通过对应的 Facade 或服务实例拿到决策器。
<?php
require_once './vender/autoload.php';
use CasbinEnforcer;
$enforcer = new Enforcer('path/to/rbac_with_domains_model.conf', 'path/to/rbac_with_domains_policy.csv');
这里实例化的 Enforcer,本质上就是权限决策器。它会读取模型配置和策略文件,然后根据传入的请求参数判断是否放行。
验证 tenant1 下的用户权限
先看商户 1,也就是 tenant1 下的用户权限:
var_dump(
$enforcer->enforce('user1', 'tenant1', 'goods1', 'read'), // true
$enforcer->enforce('user1', 'tenant1', 'goods1', 'write'), // true
$enforcer->enforce('user1', 'tenant2', 'goods2', 'read'), // false
$enforcer->enforce('user1', 'tenant2', 'goods2', 'write'), // false
);
结果很明确:user1 在 tenant1 下对 goods1 具有 read 和 write 权限;但一旦切换到 tenant2,针对 goods2 的访问就会被拒绝。
这说明权限并不是绑定在用户名本身,而是绑定在“用户 + 租户上下文”这组关系上。
验证 tenant2 下的用户权限
再看商户 2,也就是 tenant2:
var_dump(
$enforcer->enforce('user2', 'tenant1', 'goods1', 'read'), // false
$enforcer->enforce('user2', 'tenant1', 'goods1', 'write'), // false
$enforcer->enforce('user2', 'tenant2', 'goods2', 'read'), // true
$enforcer->enforce('user2', 'tenant2', 'goods2', 'write'), // true
);
同样地,user2 只有在自己的租户 tenant2 中才能访问对应商品,而不会获得 tenant1 下的权限。
从这个结果可以看出,PHP-Casbin 的域模型非常适合以下场景:
- 同一平台服务多个企业、商户或团队;
- 角色名称可能复用,但授权范围必须隔离;
- 希望继续沿用 RBAC 思路,而不是为每个租户单独维护一套权限系统。
这种设计适合什么样的 SaaS 项目
对于面向企业的 SaaS 云服务平台来说,多租户权限控制几乎是绕不开的一层基础能力。它的核心价值,不只是让多个租户共用同一套系统和组件,更重要的是在共享平台能力的同时,维持租户之间的数据隔离和授权隔离。
从实现成本看,PHP-Casbin 给出的这套方案比较直接:模型层引入 dom,策略层按租户定义角色权限,代码层通过 Enforcer 做统一判定。这样既保留了 RBAC 的易理解性,也为后续扩展多组织、多项目空间、多客户实例等能力留下了空间。
如果你的系统已经进入多客户共用平台、但又需要租户内独立授权的阶段,那么这套基于域的模型,基本就是一条实现难度和可维护性都比较平衡的路线。









