fiber 框架需手动实现多租户隔离,核心是租户中间件提取 id 并存入 c.locals,但仅此不够:数据库查询、缓存 key、日志、文件路径等均须显式加入租户维度,且租户 id 必须校验合法性。

Fiber 框架本身不内置多租户抽象,必须靠中间件 + 上下文注入 + 数据访问层配合才能实现数据隔离。它不像 Spring Boot 那样有 AbstractRoutingDataSource 或 @TenantId 注解这种框架级支持。你在 Fiber 里写一个“多租户中间件”,只是完成了租户识别这第一步,后面所有数据库操作、缓存 key 构造、日志打标都得手动接上,否则隔离就形同虚设。
怎么写一个能提取租户 ID 的 Fiber 中间件
核心是尽早从请求中解析出租户标识,并存入 c.Context,后续 handler 才能取用。常见来源有:X-Tenant-ID 请求头、子域名(如 tenant1.example.com)、路径前缀(如 /t/tenant1/users)。
示例中间件(取 header):
func TenantMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
tenantID := c.Get("X-Tenant-ID")
if tenantID == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "missing X-Tenant-ID header",
})
}
c.Locals("tenant_id", tenantID)
return c.Next()
}
}
注册方式:
app.Use(TenantMiddleware())
⚠️ 注意:c.Locals 是 request-scoped,线程安全;别用全局变量或 map 存,会串租户。
为什么只加中间件远远不够
中间件只解决了“知道是谁”,但没解决“对谁做”。如果你的数据库查询还是写死表名或没加 WHERE tenant_id = ?,那租户数据照样裸奔。常见断点包括:
- ORM 查询未自动注入租户条件(比如 GORM 的
Scopes或WithContext未传c.Locals) - 原始 SQL 拼接时直接用字符串插值,没校验租户字段是否存在或是否被绕过
- 缓存 key 没带租户前缀,
user:123被 tenantA 和 tenantB 共用 - 文件存储路径没隔离,tenantA 上传的头像覆盖了 tenantB 的
- 日志里没打租户 ID,排查问题时无法区分上下文
也就是说:中间件是开关,不是保险柜。它开了一道门,但门后每扇抽屉还得你亲手贴上标签并上锁。
GORM 场景下如何让查询自动带上 tenant_id
推荐用 GORM 的 Scopes + 中间件透传,避免每个 DAO 都重复写 Where("tenant_id = ?", tenantID)。
定义 scope:
func WithTenant(tenantID string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Where("tenant_id = ?", tenantID)
}
}
在 handler 中使用:
tenantID := c.Locals("tenant_id").(string)
var users []User
db.Scopes(WithTenant(tenantID)).Find(&users)
更进一步,可以封装一个带租户上下文的 DB 实例,或用 Session 绑定:
db.Session(&gorm.Session{Context: context.WithValue(c.Context(), "tenant_id", tenantID)}).Find(&users)
但注意:GORM 的 Session 不会自动把 context 值映射到 SQL 条件,仍需显式调用 Scopes 或在钩子中处理。
容易被忽略的缓存和日志陷阱
缓存 key 必须包含租户维度,否则 cache.Get("user:123") 可能返回 tenantB 的数据。正确写法是:
key := fmt.Sprintf("tenant:%s:user:%d", tenantID, userID)
日志也一样:用结构化日志库(如 zerolog)在每条日志里塞 TenantID 字段,而不是靠 grep 时间戳去猜哪条属于哪个租户。
最后提醒一句:租户 ID 的合法性必须校验。不能直接信任 X-Tenant-ID 头——它可能被恶意篡改。你应该查一次数据库或配置中心,确认该租户存在且当前用户有权限访问它。这步常被跳过,结果上线后租户 A 只要伪造 header 就能读 tenantB 的数据。











