可靠识别租户需在中间件中完成提取、验证、绑定三步:优先解析子域名获取租户,调用makecurrent或tenantcontext::set()绑定上下文,并置于web中间件组最前;模型层须用全局作用域(如tenantscope)自动隔离tenant_id,避免手动拼接条件;队列任务需显式传递租户上下文,测试须覆盖跨租户场景。

中间件里怎么可靠识别租户
租户识别不是“解析子域名就完事”,关键在于上下文是否在后续所有环节(模型查询、日志、队列)中持续生效。中间件是唯一可信的入口,但必须做三件事:提取、验证、绑定。
- 优先从
$request->getHost()解析子域名(如acme.example.com→acme),而不是依赖路由参数或 session —— 后者可能为空、过期或跨租户污染 - 查库后必须立刻调用
$tenant->makeCurrent()(stancl/tenancy)或写入自定义单例TenantContext::set($tenant),否则 Eloquent 仍走主连接 - 加兜底逻辑:
if (! $tenant) { throw new NotFoundHttpException(); },别让空租户流到控制器导致全量数据泄露 - 中间件顺序必须放在
$middlewareGroups['web']最前面,否则StartSession和模型全局作用域都拿不到租户上下文
模型层怎么自动隔离 tenant_id
即使中间件设好了租户,User::all() 还是查出全部用户?说明模型没真正隔离。Laravel 不会自动加 where tenant_id = ?,必须显式干预。
- 不要手动在每个
where里补条件 —— 容易漏、难维护;改用全局作用域:static::addGlobalScope(new TenantScope); - 推荐用 Trait 封装:
BelongsToTenant,在boot阶段注册作用域,并在creating事件里自动填充tenant_id字段 - 注意:
Auth::user()->tenant_id不可靠 —— 用户可能没登录,或登录态和当前租户不一致;应始终从TenantContext::id()或tenancy()->tenant()取值 - 测试时务必覆盖跨租户场景:创建租户 A/B,插入同名记录,用 A 的上下文查列表,断言只返回 A 的数据
DB::connection('tenant') 切换连接 vs 共享库加 tenant_id 字段
选哪种方案,不看文档吹嘘,看你的运维能力和增长节奏。独立数据库听着安全,但上线第一天就会暴露问题。
- 切换连接适合租户数 DB::reconnect() 频繁触发握手开销,迁移要跑 50 次,备份是 50 份文件
- 共享库加
tenant_id字段适合绝大多数 SaaS:迁移一次搞定,备份一个文件,成本低;代价是需靠代码纪律保障隔离 ——TenantContext + 全局作用域 + 强制测试缺一不可 - 别用
use Database\MyTenantSchema这种硬编码方式,租户名含下划线或大小写时直接报SQLSTATE[42000] - 如果真要用多库,
Config::set('database.connections.tenant.database', $dbName)必须在每次请求开始时重置,FPM 进程复用会导致上个请求的库名污染下一个
队列任务怎么保持租户上下文
HTTP 请求里租户上下文是天然存在的,但队列任务是异步执行的,tenant_id 不会自动带过去。默认情况下,队列任务永远在主库执行。
- 用
onConnection('tenant')或onQueue('tenant')不解决问题 —— 连接名是静态配置,不是运行时上下文 - 正确做法:在分发任务时显式传入租户标识,例如
ProcessInvoice::dispatch($invoiceId, $tenant->id),任务内部再调用TenantContext::set($tenant) - stancl/tenancy 提供
ShouldQueueAfterTenancyInitialized接口,可确保任务在租户上下文初始化后再执行 - 闭包任务(
Bus::dispatch(function () {}))默认不支持租户感知,必须包装成具名任务类,否则会丢失上下文











