应在中间件checktenant.php中最早期通过$_server['http_host']识别域名并绑定租户,动态配置数据库连接、重写模型basequery添加tenant_id过滤、隔离缓存键和session名称,全程确保租户上下文不丢失。

如何在 ThinkPHP 中通过域名识别租户
ThinkPHP 本身不内置多租户域名路由,必须手动在请求生命周期早期做域名解析和租户绑定。关键不是“配置”,而是“拦截并注入”——所有后续逻辑(如数据库连接、中间件、模型查询)都依赖这个初始判断。
常见错误是把租户识别放到控制器里,导致中间件、模型事件、日志上下文等已执行,租户上下文丢失或错乱。
- 推荐在
app/middleware/CheckTenant.php中处理,且该中间件需置于中间件队列最前端 - 用
$_SERVER['HTTP_HOST']获取原始域名(注意:反向代理下可能需检查X-Forwarded-Host) - 不要用
request()->domain(),它可能已被 URL 重写干扰;直接读$_SERVER更可靠 - 租户标识建议存入
think\facade\App::instance()->tenant或自定义容器绑定,避免全局变量污染
租户数据库连接如何动态切换
ThinkPHP 的数据库连接是“连接池级复用”,一旦初始化就固定了 database.php 配置。硬编码多个连接配置不可维护,必须在连接建立前动态改写配置项。
典型坑是:只改了 config('database.connections.tenant'),但没触发连接重建,仍复用默认连接。
- 必须在
Db::connect()调用前完成配置替换,推荐在app/middleware/CheckTenant.php中调用config(['database.connections.tenant' => $tenantConfig]) - 租户库名建议从域名映射(如
tenant1.example.com → db_tenant1),而非从数据库查——否则首次连接就依赖租户库,形成循环 - 若用 PDO,注意
charset和prefix也要按租户隔离,否则表前缀错乱会导致查询失败 - 连接名统一用
'tenant',所有模型强制指定protected $connection = 'tenant';
模型层如何避免跨租户数据泄露
即使数据库连对了,模型默认不带租户过滤,User::find(1) 可能查到其他租户的用户。这不是 Bug,是设计使然——ThinkPHP 模型不感知租户。
最容易被忽略的是软删除字段、时间戳、自动填充等全局行为,它们在多租户下可能写入错误库或漏过滤。
- 所有模型基类应重写
baseQuery()方法,在where中自动追加租户字段(如tenant_id) - 禁止在模型中使用
static::withoutGlobalScope()绕过租户过滤,除非明确知道后果 - 关联查询(
with())默认不继承主模型的租户条件,需显式在关联定义里加->where('tenant_id', $this->tenantId) - 全局作用域(Global Scope)虽可用,但 ThinkPHP 6.1+ 对它的支持不稳定,优先用基类
baseQuery
缓存与 Session 租户隔离怎么做
ThinkPHP 默认缓存键和 session ID 不含租户信息,cache('user_123') 在不同租户下会冲突。这不是配置问题,是键生成逻辑缺失。
很多人以为改 cache.prefix 就够了,但 prefix 是静态配置,无法随域名动态变化。
- 必须在
app/middleware/CheckTenant.php中设置运行时缓存前缀:Cache::setTag('tenant_' . $tenantId)或重写Cache::tag()行为 - Session 需覆盖
session.name,例如设为'PHPSESSID_' . $tenantId,否则登录态会串租户 - Redis 缓存若用同一实例,务必用不同 database(如
db 1存租户1,db 2存租户2),仅靠 key 前缀防不住误清空 - 文件缓存路径也得动态化,比如把
runtime/cache/改成runtime/cache/{tenant_id}/,否则文件锁和清理会出问题
域名多租户最难的不是识别,是让整个框架链路(路由→中间件→DB→模型→缓存→session)都信任并传递同一个租户上下文。任何一环脱节,轻则数据错乱,重则租户间越权访问。别指望一个配置开关搞定,每个环节都得亲手加固。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










