本文目录导读:

- 字段隔离(最常见,适合SaaS多租户)
- 数据库实例/Schema隔离(物理/逻辑隔离)
- 行级权限隔离(最细粒度)
- 分库分表与读写分离(大型项目)
- 数据隔离的8个最佳实践(优先级从高到低)
- 项目建议(从零开始或重构)
在PHP项目中实现数据隔离,核心取决于你的项目架构是单租户(每个用户独立数据库/表)还是多租户(共享数据库,通过字段区分)。
下面是几种常见的实现方式,从简单到复杂排列:
字段隔离(最常见,适合SaaS多租户)
所有租户(或用户)共用同一张表,通过一个租户ID字段来区分数据。
- 实现方式:在每个需要隔离的数据表中添加
tenant_id或user_id字段,每次查询时,WHERE条件自动带上该字段。 - 核心风险:必须保证所有SQL(尤其是
UPDATE、DELETE)都携带该字段,否则会泄露或修改其他用户数据。 - PHP实现技巧:
- ORM/Query Builder钩子:大多数PHP框架(Laravel、Symfony、ThinkPHP)提供了全局作用域或查询范围功能,可以自动为每个查询添加租户ID条件。
- Laravel示例:使用
Global Scope或Traits,在模型booted()方法中自动应用->where(‘tenant_id’, auth()->user()->tenant_id)。 - ThinkPHP示例:使用模型层的
scope方法,或者在基类Model的initialize方法中注入条件。 - 防漏手则:禁用原生的
Model::query()->deleteAll()或DB::table()->updateAll()方式,强制通过模型类操作。
数据库实例/Schema隔离(物理/逻辑隔离)
每个租户使用独立的数据库(Database)或独立的Schema。
- 实现方式:每个用户/租户有自己的数据库名或表前缀(如
tenant_1_orders)。 - 优点:隔离彻底,安全性高,一个租户出问题不影响其他租户。
- 缺点:维护成本高(连接数多、备份复杂)。
- PHP实现:
- 动态数据库连接:在用户登录时,基于其
tenant_id动态配置数据库连接(如config/database.php中动态返回不同主机或库名)。 - 中间件:通过中间件在请求开始时切换默认数据库连接。
- Laravel示例:使用
Config::set(‘database.connections.tenant.database’, ‘tenant_‘.$tenantId)DB::purge(‘tenant’)。
- 动态数据库连接:在用户登录时,基于其
行级权限隔离(最细粒度)
不仅基于租户,还基于用户角色、部门、数据归属链(如A部门的数据,B部门经理可见,B部门员工仅可见自己创建的数据)。
- 实现方式:数据表中增加
created_by、department_id、visibility(可见范围:本人/部门/全公司)等字段。 - PHP实现:
- 使用策略模式(Policy)或RBAC(基于角色的访问控制)+ 数据层面筛选。
- 控制层:在Service层或控制器中调用一个
PermissionHelper,根据当前用户角色动态拼装WHERE条件。 - 前端配合:前端请求时必须携带用户Token,后端解析出用户信息和角色,然后应用到数据查询中。
分库分表与读写分离(大型项目)
数据量巨大时,按租户ID或用户ID进行水平切分(Sharding)。
- 实现方式:通过中间件(如Mycat、ShardingSphere,或在PHP层面通过一致性哈希)将不同用户的数据路由到不同数据库/表。
- PHP实现:
- 代码中手动实现路由逻辑:根据
user_id % 16的结果决定查询db_0~db_15。 - 推荐使用第三方库(如
php-shard或结合ORM的sharding插件)。
- 代码中手动实现路由逻辑:根据
数据隔离的8个最佳实践(优先级从高到低)
- 所有隔离逻辑必须放在后端,不信任前端传的任何标识(租户ID应从Session/Token中获取,而非URL参数)。
- 建立统一的查询基类:所有数据库查询通过一个统一的中间层(Repository或Service)进行,避免直接在Controller里写SQL。
- 使用框架的全局过滤:Laravel的
Global Scope、ThinkPHP的where钩子、Yii2的behaviors,强制每个模型查询自动带条件。 - 写定时任务:定期扫描数据库,检查是否存在
tenant_id为NULL或与当前用户不匹配的异常数据。 - 敏感操作校验:在修改、删除数据前,必须二次校验该数据是否属于当前用户租户(即使查询条件已经带上了,也要加 assert/if)。
- 日志审计:记录谁在什么时间操作了哪条数据,方便追踪跨租户泄露问题。
- 使用数据库视图(可选):某些情况下,可以为每个租户创建一个数据库视图(如
v_orders_tenant_1),但维护较麻烦。 - API版本和RBAC结合:不同的API版本或不同权限的角色,看到的数据库列和行也不同。
项目建议(从零开始或重构)
- 小项目/快速起步:使用 字段隔离(方案1),利用ORM的全局作用域,开发效率高,代码改动最小。
- 企业级SaaS:使用 Schema隔离(方案2) + 字段隔离,在物理上隔离数据库,在逻辑上通过字段做细粒度控制。
- 金融/医疗等强合规行业:必须使用 数据库实例隔离(方案2),不能有任何数据混存。
一个通用且相对安全的实现模式(伪代码):
// 基类模型(所有模型继承这个)
abstract class BaseModel extends Model
{
protected static function booted()
{
static::addGlobalScope('tenant', function (Builder $builder) {
$tenantId = auth()->user()->tenant_id; // 从登录信息获取
$builder->where('tenant_id', $tenantId);
});
// 自动填充tenant_id(如果是新增操作)
static::creating(function ($model) {
if (empty($model->tenant_id)) {
$model->tenant_id = auth()->user()->tenant_id;
}
});
}
}
这样,大部分业务代码就不需要自己加 where('tenant_id', ...) 了,极大降低了人工遗漏的风险。