1. 多租户RBAC权限系统设计概述
在构建企业级SaaS应用时,多租户权限管理是一个绕不开的核心问题。我最近在重构公司的一个老项目时,就遇到了这个挑战——原有的单租户权限系统已经无法支撑业务扩张的需求。经过多轮技术选型和方案验证,最终选择了基于PHP-Casbin的多租户RBAC方案。
这个方案的核心价值在于:通过租户隔离字段(tenant_id)实现数据层面的天然隔离,同时利用Casbin强大的策略引擎处理复杂的权限验证逻辑。相比传统方案,它解决了三个痛点:
- 租户间数据完全隔离,避免越权访问风险
- 权限配置灵活可扩展,支持角色继承和细粒度控制
- 业务逻辑与权限验证解耦,维护成本降低50%以上
2. 核心表结构设计解析
2.1 租户表(tenant)设计要点
租户表是整个系统的根基,设计时我特别注意了这几个关键点:
sql复制CREATE TABLE `tenant` (
`id` varchar(64) NOT NULL COMMENT '租户ID(建议使用UUID或租户编码)',
`name` varchar(128) NOT NULL COMMENT '租户名称',
`status` tinyint(1) NOT NULL DEFAULT 1 COMMENT '状态:1-启用,0-禁用',
`expire_time` datetime DEFAULT NULL COMMENT '租户过期时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
实际项目中踩过的坑:
- 租户ID不要用自增数字,建议采用
tenant_前缀的编码(如tenant_001)或UUID - 过期时间字段必须可为NULL,表示永久有效
- 状态字段建议用枚举值而非布尔值,为未来扩展预留空间
2.2 用户表(sys_user)的特殊处理
用户表需要特别注意租户隔离和唯一性约束:
sql复制CREATE TABLE `sys_user` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`tenant_id` varchar(64) NOT NULL COMMENT '租户ID',
`username` varchar(64) NOT NULL COMMENT '用户名(租户内唯一)',
UNIQUE KEY `uk_tenant_username` (`tenant_id`, `username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里有个实际项目中的经验:用户名在租户内必须唯一,但不同租户可以有相同用户名。我们通过uk_tenant_username复合唯一索引实现这个约束。
2.3 角色与权限表的关联设计
角色表和权限表通过中间表实现多对多关联:
sql复制-- 角色表
CREATE TABLE `sys_role` (
`role_code` varchar(64) NOT NULL COMMENT '角色标识(租户内唯一)'
) ENGINE=InnoDB;
-- 权限表
CREATE TABLE `sys_permission` (
`perm_code` varchar(128) NOT NULL COMMENT '权限标识(如/user:GET)'
) ENGINE=InnoDB;
-- 角色-权限关联表
CREATE TABLE `sys_role_permission` (
UNIQUE KEY `uk_tenant_role_perm` (`tenant_id`, `role_id`, `perm_id`)
) ENGINE=InnoDB;
权限标识的设计技巧:
- 接口权限建议用
资源路径:HTTP方法格式(如/user:GET) - 前端菜单权限可以用
menu:模块:功能格式(如menu:system:user) - 按钮权限可以用
button:页面:按钮格式(如button:user:add)
3. Casbin策略同步机制
3.1 策略存储的双层设计
我们的方案采用业务表+策略表的双层存储:
- 业务表(用户/角色/权限):用于管理界面展示和配置
- Casbin策略表(casbin_rule):用于高性能权限验证
同步逻辑示例(给角色绑定权限时):
php复制// 业务层操作
DB::table('sys_role_permission')->insert([
'tenant_id' => 'tenant_001',
'role_id' => 1,
'perm_id' => 1
]);
// 同步到Casbin
$permission = DB::table('sys_permission')->find(1);
[$resource, $action] = explode(':', $permission->perm_code);
Casbin::addPolicy($role->role_code, 'tenant_001', $resource, $action);
3.2 策略格式规范
Casbin策略的两种核心类型:
- 用户-角色绑定(g类型):
code复制g, alice, admin, tenant_001 - 角色-权限绑定(p类型):
code复制p, admin, tenant_001, /user, GET
实际项目中的优化技巧:
- 租户ID必须作为v1或v2字段参与匹配
- 策略加载建议使用过滤适配器,只加载当前租户的策略
- 频繁变动的策略可以配合Redis缓存
4. 性能优化实战经验
4.1 数据库优化方案
经过压测我们发现三个性能瓶颈点及解决方案:
-
索引优化:
sql复制-- 必须添加的索引 ALTER TABLE `sys_user` ADD INDEX `idx_tenant_id` (`tenant_id`); ALTER TABLE `casbin_rule` ADD INDEX `idx_ptype_v0_v1_v2` (`ptype`, `v0`, `v1`, `v2`); -
查询优化:
php复制// 错误写法(导致全表扫描) User::where('username', 'alice')->first(); // 正确写法(利用复合索引) User::where('tenant_id', 'tenant_001') ->where('username', 'alice') ->first(); -
缓存策略:
php复制$permissions = Cache::remember( "tenant:{$tenantId}:user:{$userId}:perms", 3600, fn() => $user->getPermissions() );
4.2 事务处理的最佳实践
业务表与策略表的同步必须使用事务:
php复制DB::beginTransaction();
try {
// 业务表操作
$userRole = new SysUserRole($data);
$userRole->save();
// Casbin操作
Casbin::addRoleForUserInDomain(
$data['user_id'],
$data['role_code'],
$data['tenant_id']
);
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
我们遇到过的事务陷阱:
- Casbin的savePolicy()必须在事务内执行
- 跨数据库的事务需要分布式事务解决方案
- 长事务要拆分为小事务分批处理
5. 典型问题排查指南
5.1 权限不生效的排查步骤
-
检查用户-角色绑定:
sql复制SELECT * FROM sys_user_role WHERE user_id = ? AND tenant_id = ?; -
检查角色-权限绑定:
sql复制SELECT p.perm_code FROM sys_role_permission rp JOIN sys_permission p ON rp.perm_id = p.id WHERE rp.role_id = ? AND rp.tenant_id = ?; -
检查Casbin策略:
sql复制SELECT * FROM casbin_rule WHERE ptype = 'p' AND v0 = ? AND v1 = ?;
5.2 跨租户污染问题
常见症状:租户A的用户能看到租户B的数据
解决方案检查清单:
- 所有SQL查询必须包含tenant_id条件
- 接口参数必须验证租户归属
- Casbin策略必须包含租户维度
- Redis缓存key必须包含租户前缀
6. 扩展设计思路
6.1 多级租户支持
对于需要多级租户的场景(如集团-子公司),可以扩展设计:
sql复制ALTER TABLE `tenant` ADD COLUMN `parent_id` varchar(64) DEFAULT NULL COMMENT '父租户ID';
权限继承策略:
- 子租户继承父租户的角色模板
- 自定义权限会覆盖继承的权限
- 数据访问通过tenant_id like 'parent_%'实现
6.2 字段级权限控制
在现有方案上扩展字段权限:
sql复制CREATE TABLE `sys_field_permission` (
`perm_id` bigint(20) NOT NULL COMMENT '关联权限ID',
`model` varchar(50) NOT NULL COMMENT '模型名',
`field` varchar(50) NOT NULL COMMENT '字段名',
`visible` tinyint(1) DEFAULT 1 COMMENT '是否可见'
) ENGINE=InnoDB;
前端配合实现:
javascript复制// 动态过滤字段
const fields = user.permissions.filter(p =>
p.model === 'user' && p.visible
);
7. 测试数据设计技巧
7.1 模拟多租户数据
使用工厂模式生成测试数据:
php复制$tenant1 = Tenant::create(['id' => 't1', 'name' => '租户1']);
$tenant2 = Tenant::create(['id' => 't2', 'name' => '租户2']);
$user1 = User::factory()->create([
'tenant_id' => $tenant1->id,
'username' => 'alice'
]);
$user2 = User::factory()->create([
'tenant_id' => $tenant2->id,
'username' => 'alice' // 同名用户
]);
7.2 边界测试用例
必须覆盖的测试场景:
- 同一用户名在不同租户的权限隔离
- 租户停用后的权限失效
- 角色继承后的权限合并
- 权限冲突时的优先级处理
8. 部署注意事项
8.1 初始化迁移脚本
建议的迁移顺序:
- 创建租户表并初始化超级租户
- 创建业务表(用户/角色/权限)
- 创建Casbin策略表
- 初始化系统管理员角色和权限
8.2 监控指标
需要监控的关键指标:
- 策略加载时间(应<500ms)
- 权限检查耗时(应<50ms)
- 租户数据量增长趋势
- 缓存命中率(目标>90%)
这套方案在我们生产环境已经稳定运行2年,支撑了超过500个租户和10万+用户的权限管理。最大的收获是:良好的权限系统设计应该像空气一样——用户感受不到它的存在,但它无处不在保护系统安全。