最稳妥做法是在模型基类重写basequery方法自动追加tenant_id条件,但count、with等特殊查询不触发该方法,需单独处理;租户id须从可信源获取,关联查询需显式传参或启用作用域。

ThinkPHP 查询构造器里怎么加租户 ID 过滤
所有数据查询必须带上 tenant_id 条件,否则权限就漏了。不是靠中间件“拦一下”,而是每条 select、find、count 都得主动塞进去。
最稳妥的做法是统一在模型基类里重写 baseQuery 方法,而不是在每个控制器里手动写 where('tenant_id', $tid) —— 容易漏,也难审计。
- 用
protected function baseQuery($query)在基类模型中拦截所有查询,自动追加where('tenant_id', $this->getTenantId()) -
$this->getTenantId()必须从可信来源取(如请求头X-Tenant-ID或登录态缓存),不能从 URL 或表单参数读 - 注意:关联查询(
with)默认不走基类baseQuery,要对关联模型单独处理,或改用hasWhere显式传参 - 如果用了软删除,
baseQuery里还要小心别把delete_time IS NULL和租户条件搞混顺序,否则可能绕过过滤
ThinkPHP 中间件做租户识别但没生效?检查这几点
中间件只是把租户 ID 存到容器或 Request 对象里,不代表模型会自动读取——它和数据库查询之间没有隐式绑定。
常见错误是中间件里写了 think\Container::getInstance()->bind('tenant_id', $tid),但模型里直接 container('tenant_id') 取值失败,因为容器 bind 的实例生命周期不对,或者被后续覆盖。
- 推荐把租户 ID 绑定到
Request对象:$request->tenantId = $tid,然后在基类模型里通过app('request')->tenantId读取 - 确保中间件执行顺序早于路由调度和控制器实例化(在
app/middleware.php中确认位置) - 如果用了多级子域名租户(如
tenant1.example.com),注意 Nginx/Apache 是否透传了 Host 头,$_SERVER['HTTP_HOST']可能被篡改,需白名单校验
ThinkPHP 软删除 + 租户隔离时 count() 结果异常
count() 是聚合查询,不触发模型的 baseQuery 或 scope,也不会加载模型事件和属性访问器,所以租户条件很容易丢。
比如你写了 User::where('status', 1)->count(),哪怕 User 模型有 baseQuery,这个 count() 依然不会进该方法。
- 必须显式补全租户条件:
User::where('tenant_id', $tid)->where('status', 1)->count() - 或者改用
User::withoutGlobalScope()->where(...)->count()配合自定义全局作用域(但要注意:全局作用域在count()中默认仍不生效,得手动启用) - 更稳的方式是封装一个
tenantCount()方法,在基类模型里强制走完整查询链路,哪怕底层是db()->table()->count()也要先拼好 where
ThinkPHP 关联预载(with)绕过租户过滤怎么办
with('profile') 默认会查 Profile 模型的全部数据,不带任何租户条件——即使 Profile 自己也有 baseQuery,TP6.1+ 之前版本也不会自动调用。
这是因为预载是独立发起的新查询,走的是关联模型自身的查询构造器,和主模型的上下文无关。
- 给关联定义加上闭包条件:
hasOne('Profile')->where('tenant_id', $this->tenantId) - 或者在
Profile模型里定义全局作用域tenantScope,并在关联中显式启用:hasOne('Profile')->scope('tenant') - 避免用
withCount直接统计关联数量,它同样不走模型逻辑;应改用子查询或手动 join + group
租户隔离真正难的不是加一行 where,而是所有查询入口——包括 count、max、join、with、子查询、原生 SQL、甚至 Db::raw 表达式——都得被同一套规则兜住。漏掉任意一种,数据就可能跨租户可见。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










