Laravel 13 中必须在 RouteServiceProvider::mapWebRoutes() 开头、web.php 加载前用 Route::domain() 注册多租户路由,显式声明 $tenant 参数,配合 Nginx 泛解析与 .env 的 SESSION_DOMAIN 配置,并在中间件中安全提取租户初始化上下文。

Laravel 13 中用 Route::domain() 配置多租户动态路由,核心逻辑没变,但需适配新版本的路由加载机制和安全约束。关键不是“能不能用”,而是在哪注册、怎么传参、如何确保参数可用——尤其在 Laravel 13 强化了路由编译与缓存机制后,错一步就拿不到 $tenant。
✅ 必须在 RouteServiceProvider::mapWebRoutes() 开头注册
Laravel 13 仍沿用“路由注册早于 web.php 加载”的原则。所有子域名路由必须在 routes/web.php 被引入前定义,否则会被主域名路由拦截。
打开 app/Providers/RouteServiceProvider.php,定位到 mapWebRoutes() 方法,在 require base_path('routes/web.php') 之前插入:
Route::domain('{tenant}.example.com')
->where('tenant', '[a-z0-9\-]+') // 防止非法子域名(如 .. 或 /)
->group(function () {
require base_path('routes/tenant.php');
});⚠️ 注意:
- 不要用
*.example.com—— Laravel 从不支持通配符星号语法; -
where()约束必须显式加,否则{tenant}可能匹配空值或危险字符; - 这段代码不能写进
web.php,也不能放在api.php;它只对 Web 请求生效。
✅ routes/tenant.php 必须显式接收 $tenant 参数
Laravel 13 不再自动解包路由参数到闭包或控制器方法中,除非你明确声明。否则 $request->route('tenant') 可能返回 null,中间件里取不到值。
在 routes/tenant.php 中:
// ✅ 正确:闭包显式接收
Route::get('/', function (string $tenant) {
return "Welcome to {$tenant}'s workspace";
});
// ✅ 正确:控制器方法签名含 $tenant
Route::get('/dashboard', [TenantDashboardController::class, 'show']);
// TenantDashboardController.php
public function show(string $tenant)
{
// $tenant 已是解析好的子域名(如 shop、client1)
}❌ 错误示例(会报 Missing argument):
Route::get('/', function () { /* 没写 $tenant → 参数丢失 */ });✅ Web 服务器与本地环境要配合泛解析
Laravel 只负责匹配 Host 头,不负责 DNS。若请求根本没发到你的应用,后面全白搭。
Laravel Cloud(cloud.laravel.com)。此技能用于处理所有 Laravel Cloud 请求,包括搜索和读取数据。只要任务涉及 Laravel Cloud,即使用此技能。
-
Nginx 配置示例(
server_name必须覆盖):server_name ~^(?<sub>.+)\.example\.com$ *.example.com;
或更稳妥地列出常用子域:
server_name shop.example.com admin.example.com client1.example.com;
-
本地开发:在
/etc/hosts添加:127.0.0.1 shop.example.com 127.0.0.1 client1.example.com
-
APP_URL 和 SESSION_DOMAIN:
-
.env中设APP_URL=https://example.com(主域,不含子域); -
SESSION_DOMAIN=.example.com(开头带点,使 Cookie 在所有子域下共享)。
-
✅ 中间件中安全提取租户并初始化上下文
不要在控制器里手动切数据库,而应在全局中间件中统一处理,比如 InitializeTenant:
public function handle(Request $request, Closure $next)
{
$tenant = $request->route('tenant');
if (!$tenant || !Tenant::where('slug', $tenant)->active()->exists()) {
abort(404);
}
// 动态设置数据库连接名(假设你用的是 shared-db + tenant_id 字段)
config(['database.connections.tenant.database' => "tenant_{$tenant}"]);
DB::purge('tenant');
// 可选:绑定当前租户到请求属性,供后续使用
$request->attributes->set('tenant', $tenant);
return $next($request);
}别忘了在 app/Http/Kernel.php 的 $middlewareGroups['web'] 中注册该中间件。
不复杂但容易忽略。


















