thinkphp 8.0 构建 saas 多租户平台须严守三大原则:租户识别前置(子域名中间件首置+正则白名单)、数据库连接动态隔离(重建连接池+tenant专属配置)、模型全局作用域强制过滤(覆盖count/with等绕过场景),并同步隔离缓存、session、日志及sdk实例。

ThinkPHP 8.0 搭建 SaaS 多租户平台,关键不在“版本新”,而在租户识别必须前置、数据过滤必须贯穿、连接复用必须隔离——漏掉任一环节,都可能造成跨租户数据泄露,哪怕只漏一个 count() 或一次关联查询。
子域名识别必须在中间件最前端完成
不能依赖路由、控制器或模型初始化,更不能从 URL 参数、session 或 cookie 中读取 tenant_id。这些方式在 CLI 命令、队列任务、定时器中完全失效,且易被伪造。
- 在
app/middleware/CheckTenant.php中,优先读取$_SERVER['HTTP_X_FORWARDED_HOST'](反向代理场景), fallback 到$_SERVER['HTTP_HOST'];绝对不用request()->domain(),它可能已被 Nginx rewrite 规则污染 - 用正则白名单提取子域名,例如:
/^([a-z0-9\-]+)\.example\.com$/,过滤www、api等保留字,拒绝非法字符 - 将解析出的租户标识(如
tenant1)直接绑定到请求实例:$request->tenantId = $tenantId,后续所有组件(模型、日志、缓存)都从此处取值 - 该中间件必须排在
app/middleware.php的第一位,确保早于路由解析、模型加载、日志初始化等任何操作
数据库连接必须动态注册并强制重建
ThinkPHP 的 Db 连接是懒加载 + 连接池复用机制。只改配置不重建连接,旧连接仍连着默认库,极易导致写入错库、查不到数据、跨租户串查。
- 在
CheckTenant.php中,根据子域名生成租户专属数据库名(如tenant1.example.com → db_tenant1),避免首次连接就查租户表形成死循环 - 调用
Db::setConnectConfig($config)替换全局默认连接配置,其中database字段必须为完整库名,charset和prefix也需按租户单独设置 - 立即执行
Db::clearConnectionPool()清空连接池,防止复用旧连接;若追求更高性能,可重写think\db\Connection::getPoolKey(),把tenant_id显式纳入键生成逻辑 - 所有业务模型统一指定
protected $connection = 'tenant',禁止使用Db::table()绕过模型连接配置
模型层必须用全局作用域强制 tenant_id 过滤
即使连对了库,User::find(1) 仍可能查出其他租户的数据——这不是 Bug,是 ThinkPHP 模型设计使然:它本身不感知租户上下文。
- 在基类模型(如
app\model\BaseModel)的boot()方法中,显式注册全局作用域:static::addGlobalScope('tenant', new TenantScope()) -
TenantScope必须实现think\Model\ScopeInterface,其apply()方法中通过app('request')->tenantId获取可信租户 ID,再调用$builder->where('tenant_id', $tenantId) - 注意聚合查询(
count()、sum()、max())和关联查询(with())默认绕过模型生命周期,需在作用域中显式覆盖,或封装tenantCount()等专用方法 - 软删除叠加租户条件时,务必测试组合逻辑是否正确;禁用租户过滤应使用
withoutGlobalScope('tenant'),而非useGlobalScope(false)
缓存、Session、日志及第三方 SDK 必须按租户隔离命名空间
未隔离的缓存键或 Session 名称,会导致租户间数据互相覆盖;未隔离的日志路径,会让审计失去意义;未隔离的 SDK 实例(如短信、支付客户端),可能误发至错误租户。
- 缓存键统一加前缀:
cache('user_list') → cache('tenant1:user_list'),推荐封装Cache::tag('tenant1')或自定义缓存驱动 - Session 名称动态生成:
session_name('PHPSESSID_' . app('request')->tenantId),确保不同租户 Session 文件/Redis Key 完全分离 - 日志目录按租户划分:
log_path = 'runtime/log/' . app('request')->tenantId . '/',便于问题定位与权限管控 - 第三方 SDK 实例(如阿里云 OSS Client、微信支付 SDK)应在服务容器中按租户单例注册,避免凭证混用
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











