必须在中间件解析Token并注入tenant_id到请求上下文,动态切换数据库连接、模型全局作用域、缓存key均需显式携带tenant_id,否则导致数据越权或混用。

Token解析时租户ID取不到或错乱
ThinkPHP 默认不解析 Token 中的租户信息,Auth::id() 返回的是用户 ID,不是租户 ID。如果你把租户 ID 存在 JWT payload 里(比如 tenant_id 字段),但没手动提取,那后续所有「按租户隔离数据」的逻辑都会失效。
常见错误现象:DB::table('orders')->where('tenant_id', $tenantId)->get() 查出空结果,或者跨租户查到别人的数据。
- 务必在中间件里提前解析 Token,并把
tenant_id写入请求上下文,例如$request->attributes->set('tenant_id', $payload['tenant_id']) - 不要依赖全局变量或静态属性存租户 ID,ThinkPHP 的请求生命周期中,多个请求可能复用同一个 Worker 进程,静态值会污染
- 如果用的是
think-auth或自定义 Auth 驱动,确认它没覆盖或忽略原始 Token payload —— 很多封装库默认只取uid和exp
多租户场景下数据库连接动态切换失败
即使拿到了 tenant_id,直接改 Db::connect() 参数也不可靠。ThinkPHP 的连接池和查询构造器缓存会让上一个租户的连接被复用,导致 SQL 执行在错误库上。
使用场景:SaaS 系统中,每个租户独享一个数据库(如 tenant_123、tenant_456)。
- 必须在每次请求初始化阶段(如中间件末尾)调用
Db::setConfig()动态重置数据库配置,而不是只改一次database.php - 连接名要带租户标识,例如
'mysql_tenant_' . $tenantId,避免连接被复用;同时设置'deploy' => 0关闭部署模式下的连接复用 - 注意
Db::name('user')这类写法仍走默认连接,得显式指定:Db::connect('mysql_tenant_123')->name('user')
模型查询自动注入租户条件失效
想用全局作用域(Global Scope)自动加 where('tenant_id', $tenantId),但发现部分查询没生效,尤其是关联查询、with() 或原生 Db::table()。
原因很直接:ThinkPHP 的全局作用域只对模型类生效,且仅限于该模型的主表查询;一旦你写了 User::with('orders'),orders 表不会自动带租户条件,除非它也定义了同名作用域。
- 每个租户敏感模型都必须单独定义
baseScope(),并在boot()里注册,不能只在基类里写一次就以为万事大吉 -
Db::table('orders')完全绕过模型,全局作用域无效,必须手写->where('tenant_id', $tenantId) - 慎用
removeScope()—— 调试时临时去掉作用域没问题,但上线后万一漏掉,就是数据越权
Redis 缓存键未隔离租户导致数据混用
缓存 cache('user_list') 这种写法,在多租户下等于把所有租户的用户列表塞进同一个 key,A 租户刷新缓存,B 租户立刻看到脏数据。
性能影响明显:缓存命中率虚高,但内容不可信;更严重的是,缓存穿透防护(比如空值缓存)也会跨租户生效,放大风险。
- 所有缓存 key 必须包含
tenant_id,推荐格式:'tenant_' . $tenantId . '_user_list' - 不要用
Cache::tag()试图打标隔离 —— ThinkPHP 的 tag 缓存底层仍是单 key 存储,tag 只是索引,不同租户用相同 tag 依然冲突 - 如果用了 Redis Cluster,确认
tenant_id不为空字符串或非法字符,否则 key 分片异常,可能被路由到错误节点
租户识别最麻烦的地方不在解析 Token,而在于「所有数据出口」都要显式带上租户上下文 —— 模型、查询构造器、缓存、队列任务、日志埋点,甚至导出 Excel 的临时表名。少一处,就可能埋个越权隐患。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










