子域名解析必须先拆端口再切分,正确做法是用net.splithostport(r.host)提取host,再strings.splitn(host, ".", 2)获取租户前缀;主域名须定义为常量;localhost请求跳过租户提取;租户id应从jwt payload中唯一获取并贯穿全链路。

子域名解析必须先拆端口再切分
直接用 r.Host 提取租户名会崩在本地开发或带端口的请求上,比如 tenant1.example.com:8080 或 localhost:3000。不处理端口,strings.Split(host, ".") 就会把 8080 当成子域名一部分。
正确做法是先调用 net.SplitHostPort(r.Host),捕获 err 后取第一个返回值;再用 strings.SplitN(host, ".", 2) 拆出前缀和主域。主域名(如 "example.com")必须定义为常量,不能硬写进正则或字符串比较里——换域名时只改一处。
- 本地请求 Host 是
localhost或127.0.0.1时,跳过租户提取,走默认租户或报错 - 测试时用
curl -H "Host: demo.example.com" http://127.0.0.1:8080/api/users模拟子域名,别依赖浏览器自动加端口 - 千万别在 handler 里重复解析 Host —— 中间件做完就存到
c.Set("tenant_id", id),后续全靠c.MustGet("tenant_id")
Gin 不支持运行时子域名路由组
有人想写 v1 := router.Group(tenantID + "/api") 动态挂载,这行不通。Gin 的 Group() 是构建期静态注册,路由树一旦 r.Run() 就冻结了,没法按请求 Host 实时生成新分组。
真正能落地的是“一套路由、多套上下文”:所有租户共用 /api/users 这类路径,但每个 handler 开头立刻从 context 取 tenant_id,再传给 DB 查询、缓存键拼接、限流器初始化。
- 不要用
switch tenantID分支写不同逻辑 —— 把租户相关行为封装成结构体方法,比如tenantDB(tenantID).Users().List() - 避免在 handler 里查租户有效性(比如查数据库确认租户是否启用),这个动作必须前置到中间件,失败直接
c.AbortWithStatusJSON(404, ...) -
c.Param("id")和c.Query("page")不受子域名影响,该取照取,但所有下游调用必须显式携带 tenant 上下文
缓存键漏 tenant_id 是静默数据污染
Redis 里存 "user:123" 而不是 "tenant:abc:user:123",多个租户查同一个 ID 就会互相覆盖。这不是并发问题,是设计缺陷 —— 第一个租户写入,第二个租户读缓存 miss 后查 DB,却没把带 tenant_id 的结果回填,第三个租户就命中脏数据。
硬编码拼接比序列化 struct 更可靠:Go 的 struct 字段顺序、空字段处理、tag 差异都可能导致 hash 不一致。直接写 "tenant:" + tenantID + ":user:" + userID,一眼看清边界。
- 所有缓存操作(Get/Set/Del)前,先确保 key 包含
tenant_id,哪怕 key 看起来“全局唯一”(比如配置名"feature_flag:paywall") - 用
redis-go的Get返回 nil 时,务必查 DB 并用带 tenant_id 的 key 回填,否则缓存层就形同虚设 - groupcache 或 ristretto 这类本地缓存也一样 —— tenant_id 必须参与 key 构造,否则单机多租户场景下照样错乱
JWT payload 才是唯一可信 tenant_id 来源
从 X-Tenant-ID header 或 query 参数读 tenant_id 是危险起点,等于把租户边界交给不可信输入。攻击者删掉 auth header、伪造 header 就能越权访问其他租户数据。
真正可信的只有登录时签发的 JWT 里已验证的 tenant_id 字段。JWT 必须由服务端签名,且校验通过后才解出 payload —— 中间件里别重新解析、别转换类型、别 fallback 到其他来源。
- 登录成功后,JWT payload 明确包含
tenant_id和tenant_role,这两个字段直接注入ctx,后续全链路只读不改 - 别在 middleware 里做 “如果 header 有 X-Tenant-ID 就用它,没有就 fallback 到 JWT” —— fallback 路径就是绕过认证的后门
- 数据库层(如 ent)封装
WithTenant(ctx)方法,自动在所有查询的 WHERE 条件里加tenant_id = ?,比每个 repo 方法手动拼更防漏
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











