gin不支持二级域名路由分组,需通过中间件解析host头提取tenantid并注入上下文,nginx负责子域名转发与透传host头。

GIN 本身不支持二级域名路由分组
Gin 的 Group() 是构建期行为,只处理路径前缀,完全不感知 Host 头或子域名。你写 r.Group("tenant1.example.com") 不会生效——Gin 会把它当普通字符串路径注册,最终匹配的是 GET /tenant1.example.com/xxx,而非按域名分发请求。
常见错误是试图用 router.Any("*.example.com", handler) 或正则路径模拟子域名路由,这在 Gin 中语法非法,直接 panic。
真正能响应二级域名的,只有 HTTP 层的 *http.Request.Host 字段。所有子域名识别逻辑必须手动提取、校验、注入上下文,不能靠路由定义“自动分流”。
从 Host 头安全提取 tenantID 的关键步骤
直接读 r.Host 风险很高:开发环境可能是 localhost:3000,生产环境带端口如 tenant1.example.com:443,硬切点会错。
- 先用
net.SplitHostPort(r.Host)拆出纯 host,失败时直接用原值(如 localhost) - 主域名必须定义为常量,比如
const mainDomain = "example.com",不能写死在strings.Contains(r.Host, "example.com")里 - 再用
strings.SplitN(host, ".", 2)拆两段,检查第二段是否等于mainDomain,才是合法租户子域名 - 本地调试时建议加白名单:
if host == "localhost" || host == "127.0.0.1" { tenantID = "dev" }
租户上下文必须在中间件中完成校验与注入
不要在每个 handler 里重复查数据库确认租户是否存在。把校验逻辑收拢到一个中间件里,失败就 c.AbortWithStatusJSON(404, ...),成功则 c.Set("tenant_id", tenantID)。
后续所有 DB 查询、缓存键、配置加载都必须显式使用这个 tenant_id,例如 db.Where("tenant_id = ?", c.GetString("tenant_id")).Find(&users)。
注意:c.Param() 和 c.Query() 完全不受子域名影响,该取参数照取;但任何外部依赖(DB/Redis/S3)都必须带上租户隔离维度,否则就是数据越界。
Nginx 层该做而 Gin 不该做的边界
Gin 只负责“识别租户并传递上下文”,不负责“把 client1.example.com 的请求转发给哪台实例”。后者是 Nginx 的事。
典型 Nginx 配置应包含:
- 泛解析 DNS 确保
*.example.com指向服务 IP - 用
server_name ~^(?<subdomain>[^.]+)\.example\.com$</subdomain>捕获子域名 - 可选:用
map $host $upstream将不同子域名映射到不同后端集群 - 务必
proxy_set_header Host $host,否则 Gin 收到的r.Host是 upstream 地址,不是原始域名
最易忽略的一点:Nginx reload 后,Gin 进程不会自动感知新域名,但也不需要——只要 Host 头透传正确,Gin 的中间件就能继续工作。域名增删只改 Nginx 配置,不碰 Go 代码。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











