Db::setConnectConfig() 必须在全局中间件首位调用,通过HTTP_HOST提取并清洗租户ID后绑定连接配置;聚合查询会绕过全局作用域,需封装tenantCount()等方法显式添加tenant_id条件。

Db::setConnectConfig() 必须在中间件最前端调用
租户识别和数据库配置绑定不能晚于路由解析,否则模型、日志、缓存等组件可能已用默认连接初始化,导致数据写错库或查询越界。常见错误是把 Db::setConnectConfig() 放在控制器里,或者依赖 session() 或 input('tenant_id') —— CLI 命令、队列任务、定时器根本没 session,URL 参数还容易被伪造。
正确做法是在全局中间件(如 app/middleware/TenantResolve.php)中,且必须注册在 app/middleware.php 的首位:
- 优先从
$_SERVER['HTTP_X_FORWARDED_HOST']或$_SERVER['HTTP_HOST']提取子域名(如tenant1.example.com→tenant1) - 清洗租户 ID:用
preg_replace('/[^a-zA-Z0-9_]/', '_', $id)去除非法字符,再加前缀如'db_' . $safeId - 生成完整连接配置后,立刻调用
Db::setConnectConfig('tenant_' . $safeId, $config) - 把租户 ID 绑定到请求属性:
$request->withAttr('tenant_id', $safeId)
Db::connect() 单次调用不改变默认查询链路
Db::connect($config)->table('users')->select() 看似切换了,但后续所有 Model::find()、Db::table() 仍走原始连接池——因为 Db::connect() 返回的是新实例,不会覆盖门面的默认行为。这不是 Bug,是 ThinkPHP 连接池机制的设计逻辑。
真正生效的方式只有两种:
立即学习“PHP免费学习笔记(深入)”;
- 全程使用该实例链式调用,且禁止混用
Db::table()等静态门面方法 - 改用
Db::setConnectConfig()注册标识,再在模型中显式调用Db::name('users')->connect('tenant_123') - 所有业务模型强制声明
protected $connection = 'tenant';,避免漏掉连接指定 - 若用
Db::connect($config),必须确保$config中包含唯一字段(如'tenant_id' => $id),否则多个租户配置仅 database 不同会导致 MD5 键冲突,复用旧连接
动态库名拼接必须满足 MySQL 库名规则
直接把邮箱、UUID 或含特殊字符的租户标识拼进 database 配置项,PDO 会报错:SQLSTATE[HY000] [1049] Unknown database 'tenant@abc.com'。MySQL 库名不允许 @、.、- 等符号,也不能以数字开头,最大长度 64 字符。
安全拼接步骤:
- 先正则清洗:
preg_replace('/[^a-zA-Z0-9_]/', '_', $rawId) - 加固定前缀,如
'tenant_' . $cleanId - 若清洗后仍超长,截断并哈希:
'tenant_' . substr(md5($rawId), 0, 8) - 严禁用
request()->domain()构建库名——它可能被 Nginx rewrite 规则污染,应始终信任$_SERVER['HTTP_HOST']
全局作用域无法覆盖 count()、sum() 等聚合查询
即使模型启用了全局作用域,User::where('status', 1)->count() 也会绕过模型生命周期,直通查询构造器,导致 tenant_id 条件丢失。这是 ThinkPHP 的设计限制,不是配置遗漏。
应对方式很实际:
- 基类模型中封装
tenantCount()方法,内部手动加条件:$this->db()->table($this->getTable())->where('tenant_id', $this->getTenantId())->where(...)->count() - 所有聚合操作统一走该方法,禁止裸调
count() - 对软删除 + tenant_id 双条件场景,务必在
baseQuery()中显式叠加,不能只靠useSoftDelete() - 跨租户统计需明确进入超级管理员上下文,禁用租户过滤,而非临时
withoutGlobalScope()—— 后者易被忽略或误用
最关键的细节往往藏在连接键生成和聚合查询绕过这两处:前者导致数据串查难以复现,后者让隔离看似生效实则漏洞百出。上线前必须用非法租户 ID 和空租户场景做压测,不能只验证“正常流程”。



















