基于Caddy的多租户SaaS域名映射失效及Laravel路由问题排查
解决Laravel多租户SaaS自定义CNAME域名路由匹配问题
问题根源
你的路由规则硬绑定了{subdomain_slug}.{primary_domain}格式的域名,当客户通过CNAME映射自有域名(如support.waitlist.guru)时,请求的Host头是该自定义域名,而非应用主域名的子域格式,导致Laravel无法匹配路由,最终返回404或跳转到主域首页。
解决方案
1. 移除路由的固定域名约束
修改路由配置,去掉domain()的硬绑定,转而通过中间件动态识别子域:
// 移除原路由的domain约束,通过中间件处理子域识别 Route::middleware(['appendSubdomainInfo', 'subdomainAccessChecker'])->group(function () { Route::get('/check', function($subdomain_slug) { dd($subdomain_slug); }); // 其他子域路由... });
2. 更新中间件实现动态子域映射
首先在数据库新增domain_bindings表,存储客户自定义域名与应用子域的对应关系(例如support.waitlist.guru对应waitlist),然后修改appendSubdomainInfo中间件,根据请求Host查询映射关系:
<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; use App\Models\DomainBinding; class AppendSubdomainInfo { public function handle(Request $request, Closure $next) { $host = $request->getHost(); $subdomainSlug = null; // 优先匹配自定义域名绑定 $binding = DomainBinding::where('custom_domain', $host)->first(); if ($binding) { $subdomainSlug = $binding->subdomain_slug; } // 处理原生应用子域(如waitlist.layoff.wtf) else { $domainParts = explode('.', $host); $primaryDomain = config('app.primary_domain'); if (count($domainParts) >= 3 && implode('.', array_slice($domainParts, -2)) === $primaryDomain) { $subdomainSlug = $domainParts[0]; } } // 将子域信息注入请求和路由参数 if ($subdomainSlug) { $request->attributes->set('subdomain_slug', $subdomainSlug); $request->route()->setParameter('subdomain_slug', $subdomainSlug); } return $next($request); } }
3. 确保Caddy正确传递Host头
显式配置Caddy的php_fastcgi模块,保证Laravel能获取到真实的客户域名:
{ on_demand_tls { ask http://layoff.wtf/caddy/ask interval 2m burst 5 } } https:// { tls { on_demand } root * /home/forge/layoff.wtf/public file_server php_fastcgi unix//run/php/php8.2-fpm.sock { env HTTP_HOST {host} } }
4. 完善子域权限验证
在subdomainAccessChecker中间件中,验证子域的合法性:
public function handle(Request $request, Closure $next) { $subdomainSlug = $request->attributes->get('subdomain_slug'); if (!$subdomainSlug) { // 无有效子域,跳转主域或返回403 return redirect(config('app.url')); } // 验证子域对应的租户是否存在且可用 $tenant = \App\Models\Tenant::where('slug', $subdomainSlug)->first(); if (!$tenant || !$tenant->is_active) { abort(403, 'Invalid or inactive subdomain'); } // 将租户实例注入请求,供后续业务逻辑使用 $request->attributes->set('tenant', $tenant); return $next($request); }
5. 客户CNAME配置说明
告知客户只需将自定义子域通过CNAME记录指向对应应用子域(如support.waitlist.guru CNAME到waitlist.layoff.wtf),无需额外配置,应用会自动识别并路由到对应子域内容。
内容的提问来源于stack exchange,提问作者TheBigK
相关产品推荐
相关产品推荐

