php方案 PHP的多租户数据隔离

发布时间:2026/5/20 11:42:21

php方案 PHP的多租户数据隔离 三种方式分开讲---一、同库 tenant_id 列全局 Scope composerrequireilluminate/database illuminate/events?phprequirevendor/autoload.php;useIlluminate\Database\Capsule\ManagerasDB;useIlluminate\Database\Eloquent\{Builder,Model,Scope};$capsulenewDB;$capsule-addConnection([driversqlite,database:memory:]);$capsule-setAsGlobal();$capsule-bootEloquent();DB::statement(CREATE TABLE orders (id INT, tenant_id INT, amount INT));DB::table(orders)-insert([[id1,tenant_id1,amount100],[id2,tenant_id2,amount200],]);classTenantScopeimplementsScope{publicfunctionapply(Builder$b,Model$m):void{$b-where(tenant_id,$GLOBALS[tenant_id]);}}classOrderextendsModel{public$timestampsfalse;protectedstaticfunctionbooted():void{static::addGlobalScope(newTenantScope());static::creating(fn($m)$m-tenant_id$GLOBALS[tenant_id]);}}$GLOBALS[tenant_id]1;print_r(Order::all()-toArray());// 只出 tenant_id1 的数据逐行解释classTenantScopeimplementsScope实现 Scope 接口这是 Eloquent 的全局查询过滤器协议。publicfunctionapply(Builder$b,Model$m):void{$b-where(tenant_id,$GLOBALS[tenant_id]);}每次查询自动追加WHEREtenant_id?不用手写忘了也不会漏。static::addGlobalScope(newTenantScope());把 Scope 挂到模型上之后Order::all()、Order::find()全部自动带过滤。static::creating(fn($m)$m-tenant_id$GLOBALS[tenant_id]);写入时自动填 tenant_id防止忘记手动赋值导致数据串库。---二、独立数据库动态连接切换?phprequirevendor/autoload.php;useIlluminate\Database\Capsule\ManagerasDB;$capsulenewDB;$capsule-setAsGlobal();$capsule-bootEloquent();functionswitchTenant(int$tenantId):void{DB::addConnection([driversqlite,database/data/tenant_{$tenantId}.db,],tenant_{$tenantId});DB::setDefaultConnection(tenant_{$tenantId});}switchTenant(42);$ordersDB::table(orders)-get();print_r($orders-toArray());逐行解释functionswitchTenant(int$tenantId):void每次请求进来根据租户ID切换数据库连接后续所有查询都走这个租户的库。DB::addConnection([driversqlite,database/data/tenant_{$tenantId}.db,],tenant_{$tenantId});动态注册一个新连接连接名用租户ID区分。生产环境把 sqlite 换成 mysqldatabase 换成对应的库名。DB::setDefaultConnection(tenant_{$tenantId});把这个连接设为默认之后DB::table()、Eloquent 查询全走这里不用每次指定连接名。---三、PostgreSQLRLS行级安全 先在 PostgreSQL 建策略只需一次ALTERTABLEordersENABLEROWLEVELSECURITY;CREATEPOLICYtenant_policyONordersUSING(tenant_idcurrent_setting(app.tenant_id)::int);PHP侧?php$pdonewPDO(pgsql:host127.0.0.1;dbnameapp,appuser,pass);$tenantId5;$pdo-exec(SET app.tenant_id {$tenantId});$rows$pdo-query(SELECT * FROM orders)-fetchAll(PDO::FETCH_ASSOC);print_r($rows);逐行解释ALTERTABLEordersENABLEROWLEVELSECURITY;给表开启行级安全开了之后没有策略覆盖的行默认全部不可见。CREATEPOLICYtenant_policyONordersUSING(tenant_idcurrent_setting(app.tenant_id)::int);策略规则只有 tenant_id 等于当前会话变量 app.tenant_id 的行才可见。::int是 PostgreSQL 的类型转换因为 current_setting 返回字符串。$pdo-exec(SET app.tenant_id {$tenantId});设置当前连接的会话变量PostgreSQL 的RLS策略会读这个值。这一行之后这条连接上的所有查询都被数据库层自动过滤PHP代码里完全不用写WHEREtenant_id?。$rows$pdo-query(SELECT * FROM orders)-fetchAll();直接查全表但数据库只返回属于当前租户的行隔离在DB层完成PHP层感知不到。---三种方式对比一句话 ┌────────────────┬────────────┬──────────────────────────┐ │ 方式 │ 隔离在哪层 │ 适合场景 │ ├────────────────┼────────────┼──────────────────────────┤ │GlobalScope │ORM层 │ 小项目共库 │ ├────────────────┼────────────┼──────────────────────────┤ │ 动态连接 │ 连接层 │ 租户数据量大需物理隔离 │ ├────────────────┼────────────┼──────────────────────────┤ │ PostgreSQLRLS│ 数据库层 │ 安全要求高防应用层漏洞 │

相关新闻