基于Laravel+Forge搭建支持CNAME关联的类Shopify多子域名应用问询
嘿,看起来你已经搭好了基础的多租户子域名架构,我之前在做类似Shopify的Laravel多租户项目时,正好处理过这些问题,给你分享一些实操经验和完善方案:
一、核心架构完善建议
1. 强化多租户数据隔离
Laravel Spark默认是单租户逻辑,你需要扩展成严格的多租户隔离:
- 新增
Tenant模型,与用户、商品、订单等业务模型通过tenant_id关联,给所有关联字段加数据库索引 - 全局查询作用域:给所有业务模型添加全局作用域,自动过滤当前租户的数据,避免跨租户访问
- 中间件验证:在子域名路由组的前置中间件里,必须验证当前子域名对应的租户存在,否则返回404或引导至主站注册页
2. 子域名路由与容器绑定
在routes/web.php里用域名分组隔离租户路由:
Route::domain('{tenant}.app.com')->middleware('tenancy')->group(function () { // 租户专属路由:店铺首页、商品管理、订单查看等 Route::get('/', 'Tenant\HomeController@index'); Route::resource('products', 'Tenant\ProductController'); });
在HandleTenancy中间件里解析租户并绑定到容器,后续控制器可直接调用:
public function handle(Request $request, Closure $next) { $tenantSlug = $request->route('tenant'); $tenant = Tenant::where('slug', $tenantSlug)->firstOrFail(); // 将租户实例绑定到容器,全局可用 app()->instance(Tenant::class, $tenant); return $next($request); }
3. 缓存与会话隔离
多租户场景下必须避免资源冲突:
- 缓存:给所有缓存键添加租户ID前缀,或使用缓存标签
Cache::tags([$tenant->id])->put(...) - 会话:修改
config/session.php,将cookie配置为动态前缀,比如'cookie' => "session_{$tenant->id}" - 静态资源:用户上传的店铺logo、商品图建议用独立存储桶(比如S3)+ CDN,不要存在本地,避免子域名静态资源路由冲突
4. 子域名唯一性校验
用户注册生成子域名时,要确保slug唯一:
- 将公司名称转成URL友好的slug(用
Str::slug()) - 检查数据库中是否存在相同slug,若存在则自动追加后缀(比如
company-2.app.com) - 注册表单实时校验slug可用性,避免用户提交后才报错
二、CNAME关联实现方案
1. 用户侧配置流程
引导用户完成以下操作:
- 在自己的DNS服务商后台,添加一条CNAME记录,将自定义域名(比如
store.example.com)指向其专属子域名(company.app.com) - 可选:添加TXT记录完成域名所有权验证(比如
_verify.store.example.com= 系统生成的随机令牌)
2. 应用侧绑定与验证
不要依赖实时解析CNAME(DNS缓存会导致延迟和性能问题),建议提前绑定:
- 在租户后台添加「自定义域名」功能,让用户输入自己的域名
- 系统自动验证:检查该域名的CNAME是否指向正确的租户子域名,或校验TXT记录是否匹配
- 验证通过后,将自定义域名存储到
Tenant模型的custom_domain字段
3. Nginx配置调整
Laravel Forge默认的服务器块是指定具体域名的,需要修改成支持所有域名:
server { listen 80 default_server; listen [::]:80 default_server; listen 443 ssl default_server; listen [::]:443 ssl default_server; server_name _; # 匹配所有域名 root /home/forge/app.com/public; # 通配符SSL证书(覆盖*.app.com),自定义域名建议让用户用Cloudflare SSL ssl_certificate /etc/nginx/ssl/app.com/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/app.com/privkey.pem; location / { try_files $uri $uri/ /index.php?$query_string; } # 其他默认配置... }
4. 自定义域名路由处理
扩展HandleTenancy中间件,支持自定义域名解析:
public function handle(Request $request, Closure $next) { $host = $request->getHost(); // 优先匹配自定义域名 $tenant = Tenant::where('custom_domain', $host)->first(); if ($tenant) { app()->instance(Tenant::class, $tenant); return $next($request); } // 匹配默认子域名格式 if (str_ends_with($host, '.app.com')) { $tenantSlug = str_replace('.app.com', '', $host); $tenant = Tenant::where('slug', $tenantSlug)->firstOrFail(); app()->instance(Tenant::class, $tenant); return $next($request); } // 未匹配到任何租户,引导至主站 return redirect('https://app.com'); }
三、额外优化与注意事项
- 日志隔离:修改Monolog配置,给每个租户的日志添加
tenant_id标识,或按租户拆分日志文件,方便排查问题 - 本地开发测试:修改本地
hosts文件,添加127.0.0.1 test.app.com来模拟子域名访问 - 性能优化:给
tenant_id字段加复合索引(比如tenant_id + created_at),避免多租户场景下的全表扫描 - SSL兼容性:自定义域名建议让用户通过Cloudflare等CDN配置SSL,避免你的服务器需要维护大量自定义域名证书
内容的提问来源于stack exchange,提问作者Nick Maddren
相关产品推荐
相关产品推荐

