PHP项目怎么实现数据隔离?

wen java案例 1

本文目录导读:

PHP项目怎么实现数据隔离?

  1. 字段隔离(最常见,适合SaaS多租户)
  2. 数据库实例/Schema隔离(物理/逻辑隔离)
  3. 行级权限隔离(最细粒度)
  4. 分库分表与读写分离(大型项目)
  5. 数据隔离的8个最佳实践(优先级从高到低)
  6. 项目建议(从零开始或重构)

在PHP项目中实现数据隔离,核心取决于你的项目架构是单租户(每个用户独立数据库/表)还是多租户(共享数据库,通过字段区分)。

下面是几种常见的实现方式,从简单到复杂排列:

字段隔离(最常见,适合SaaS多租户)

所有租户(或用户)共用同一张表,通过一个租户ID字段来区分数据。

  • 实现方式:在每个需要隔离的数据表中添加 tenant_iduser_id 字段,每次查询时,WHERE 条件自动带上该字段。
  • 核心风险必须保证所有SQL(尤其是UPDATEDELETE)都携带该字段,否则会泄露或修改其他用户数据。
  • PHP实现技巧
    • ORM/Query Builder钩子:大多数PHP框架(Laravel、Symfony、ThinkPHP)提供了全局作用域查询范围功能,可以自动为每个查询添加租户ID条件。
    • Laravel示例:使用 Global ScopeTraits,在模型 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_bydepartment_idvisibility(可见范围:本人/部门/全公司)等字段。
  • PHP实现
    • 使用策略模式(Policy)或RBAC(基于角色的访问控制)+ 数据层面筛选。
    • 控制层:在Service层或控制器中调用一个 PermissionHelper,根据当前用户角色动态拼装 WHERE 条件。
    • 前端配合:前端请求时必须携带用户Token,后端解析出用户信息和角色,然后应用到数据查询中。

分库分表与读写分离(大型项目)

数据量巨大时,按租户ID或用户ID进行水平切分(Sharding)。

  • 实现方式:通过中间件(如Mycat、ShardingSphere,或在PHP层面通过一致性哈希)将不同用户的数据路由到不同数据库/表。
  • PHP实现
    • 代码中手动实现路由逻辑:根据 user_id % 16 的结果决定查询 db_0db_15
    • 推荐使用第三方库(如 php-shard 或结合ORM的sharding插件)。

数据隔离的8个最佳实践(优先级从高到低)

  1. 所有隔离逻辑必须放在后端,不信任前端传的任何标识(租户ID应从Session/Token中获取,而非URL参数)。
  2. 建立统一的查询基类:所有数据库查询通过一个统一的中间层(Repository或Service)进行,避免直接在Controller里写SQL。
  3. 使用框架的全局过滤:Laravel的Global Scope、ThinkPHP的where钩子、Yii2的behaviors,强制每个模型查询自动带条件。
  4. 写定时任务:定期扫描数据库,检查是否存在 tenant_id 为NULL或与当前用户不匹配的异常数据。
  5. 敏感操作校验:在修改、删除数据前,必须二次校验该数据是否属于当前用户租户(即使查询条件已经带上了,也要加 assert/if)。
  6. 日志审计:记录谁在什么时间操作了哪条数据,方便追踪跨租户泄露问题。
  7. 使用数据库视图(可选):某些情况下,可以为每个租户创建一个数据库视图(如 v_orders_tenant_1),但维护较麻烦。
  8. 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', ...) 了,极大降低了人工遗漏的风险。

抱歉,评论功能暂时关闭!