gin中间件不能在闭包或全局变量中存储租户db实例——必须从jwt claims等可信源推导租户id,用sync.map缓存各租户专属*sql.db,再通过context.withvalue注入请求上下文,handler中显式获取使用,避免伪造、泄漏与跨租户数据泄露。

中间件里不能存租户 DB 实例
框架中间件本身不持有 *sql.DB,也不能用全局变量缓存租户连接。否则并发请求会互相覆盖、事务错乱,甚至导致连接泄漏。关键约束是:每个请求生命周期内必须独占一个租户 DB 实例,且不能跨请求复用。
- 错误做法:在中间件闭包外定义
var tenantDB *sql.DB,然后在Handle里赋值 - 正确路径:从请求上下文提取租户标识 → 查缓存或新建连接 → 注入到
echo.Context的Set()中 - 租户标识来源常见有三种:
X-Tenant-IDheader、子域名(如tenant1.example.com)、或路由参数(如/t/abc/users)
如何安全提取并验证租户标识
别直接信任 header 或子域名,必须做白名单校验和存在性检查。比如从 X-Tenant-ID 取值后,要确认该租户在系统数据库中已激活,且未被软删除。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
- 用
c.Request().Header.Get("X-Tenant-ID")获取值,空字符串或非法格式立即返回echo.NewHTTPError(400, "missing tenant ID") - 子域名解析建议用
net.ParseIP()+strings.Split(host, ".")提取前缀,再查tenants表的subdomain字段 - 避免正则硬匹配,比如
^([a-z0-9]+)\.example\.com$容易被绕过;优先走数据库查证
怎么把租户 DB 注入到 handler 生命周期
必须在中间件里调用 c.Set("tenant_db", db),而不是传参或设全局变量。handler 里通过 c.Get("tenant_db") 拿,类型断言为 *sql.DB 即可使用。
- 不要在 handler 里重复查租户库——中间件已查过一次,结果应缓存到 context
- 如果租户 DB 创建耗时(如首次连接),建议用
sync.Pool缓存连接池,但 Pool Key 必须含租户唯一标识 - 务必在中间件末尾加
defer db.Close()?错。DB 连接池是长生命周期对象,Close()会关掉整个池,应由应用启动时统一管理生命周期
为什么不能依赖路由参数透传租户信息
路由参数(如 /t/:tenant_id/users)看似直观,但容易被绕过或污染。比如攻击者构造 /t/../admin/settings 触发路径遍历,或伪造参数跳过权限校验。
- 租户识别必须发生在所有业务逻辑之前,且不可被后续中间件或 handler 修改
- 若必须用路由参数,应在中间件开头就提取并验证,验证失败立刻中断,不往下走
next(c) - 更安全的做法是绑定到 TLS ClientHello 的 SNI 域名,或 JWT claim 中的
tenant_id字段,这两者无法被客户端随意篡改
Set() 或错写 key 名,下游所有 DAO 层都会 panic。










