租户识别必须在中间件最前端完成,需从http_host或x_forwarded_host提取域名并绑定至app::instance(),严禁依赖session或url参数;数据库连接须动态重置并清空缓存,模型须通过全局作用域强制tenant_id过滤,缓存、session、日志及第三方sdk均需按租户隔离命名空间。

租户识别必须在中间件最前端完成
租户识别不是“配置一下就行”,而是整个多租户链路的起点。一旦晚于请求生命周期早期,模型事件、日志记录、缓存写入等都可能已执行,导致上下文错乱或数据泄露。
常见错误是把 tenant_id 从 session() 或 input('tenant_id') 里读——CLI 命令、队列任务、定时器根本没 session,且 URL 参数极易被篡改。
- 用
$_SERVER['HTTP_HOST']读原始域名(子域名如tenant1.example.com),反向代理下优先检查$_SERVER['HTTP_X_FORWARDED_HOST'] - 绝对不要用
request()->domain(),它可能已被 rewrite 规则污染 - 识别结果必须写入可信上下文:推荐绑定到
think\facade\App::instance(),例如App::instance()->tenant = $tenantId - 该中间件(如
app/middleware/CheckTenant.php)必须注册在app/middleware.php的最顶部
数据库连接必须动态重置并强制重建
ThinkPHP 的数据库连接是“懒初始化 + 连接池复用”,只改 config('database.connections.tenant') 不会生效——旧连接仍连着默认库。
典型现象:查不到数据、写入到公共库、Db::connect('tenant') 返回的还是旧连接。
- 在
CheckTenant.php中调用config(['database.connections.tenant' => $config])后,必须显式清空连接缓存:Db::clearConnection('tenant') - 租户库名建议直接由域名映射生成(如
tenant1.example.com → db_tenant1),避免首次连接就查租户表形成死循环 - 所有模型统一指定
protected $connection = 'tenant',禁止用Db::table()绕过模型连接配置 - PDO 连接需同步更新
charset和prefix,否则跨租户表前缀错位会导致Table not found
模型层必须用全局作用域强制 tenant_id 过滤
即使连对了库,User::find(1) 仍可能查出其他租户的用户——ThinkPHP 模型本身不感知租户,这是设计使然,不是 Bug。
软删除叠加租户过滤时尤其危险:delete_time IS NULL AND tenant_id = ? 的组合逻辑若未显式控制,count() 可能返回 0 却实际有数据。
- 在基模型
app\model\BaseModel的boot()中注册:static::addGlobalScope('tenant', new TenantScope()) -
TenantScope必须实现think\Model\ScopeInterface,apply()中从App::instance()->tenant读取 ID,严禁调用session()或cookie() - 关联查询(
with())默认不继承主模型租户条件,需在关联定义中显式加->where('tenant_id', $this->tenant_id) - 后台管理等跨租户场景,用
withoutGlobalScope('tenant'),而非useGlobalScope(false)(会禁用全部作用域)
缓存、Session、日志必须按租户隔离命名空间
共享缓存键或 Session 名称会导致租户间互相覆盖,比如租户 A 登录后,租户 B 刷新页面直接拿到 A 的登录态。
日志混在一起还会让问题排查变成灾难:你永远不知道那条“订单创建失败”属于哪个租户。
- 缓存键前缀统一加
tenant_{$tenantId}_,例如cache('tenant_123_user_list', $data) - Session 名称不能用默认
PHPSESSID,需动态设置:session_name('sess_' . $tenantId),并在中间件中调用 - 日志通道名、文件路径均需含租户标识,例如
Log::channel('tenant_' . $tenantId)->info('order created') - 所有第三方 SDK(如 Redis 客户端、Elasticsearch 实例)也需确认是否支持租户级命名空间,否则要手动封装
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











