不能只做一次context.withvalue,因go context不跨goroutine传播;异步任务须用isolatedctx显式传tenant_id,redis key需租户前缀,weaviate必须启用tenant-aware collections并每次操作指定tenant。

中间件里提取 tenant_id 不能只做一次 context.WithValue
Iris 的中间件中调用 c.Request.Context() 获取上下文后,用 context.WithValue 注入 tenant_id,只是给当前 HTTP 请求 goroutine 绑定了值。一旦启动异步任务(比如 go func() { ... }())、调用定时器、触发 webhook 或发消息到队列,新协程就拿不到这个值——Go 的 context 默认不跨 goroutine 传播。
常见错误现象:go processOrder(order) 里查 Redis 缓存或写 Weaviate 向量,结果用了上一个请求残留的 tenant_id,导致 A 租户的数据被 B 租户的操作覆盖。
- 所有异步逻辑前必须显式构造干净上下文:
isolatedCtx := context.WithValue(context.Background(), "tenant_id", tenantID) - 数据库、Redis、Weaviate 客户端层要统一从该
isolatedCtx中取tenant_id,禁止 fallback 到闭包变量或全局 map - 不要在中间件里写
c.Set("tenant_id", val)然后各处去c.Get("tenant_id"),这仅限当前请求生命周期,对下游服务无效
Redis Key 必须带租户前缀,且不能手拼
直接用 "prompt:12345" 这类无租户标识的 key,会导致不同租户对同一 ID 的缓存互相污染。A 租户更新 prompt 后,B 租户下次读到的就是脏数据。
正确格式是 "tenant:org-456:prompt:12345",其中 org-456 来自当前请求解析出的租户标识。但别在每个 redis.Get 前手动拼字符串——易漏、难维护、测试难覆盖。
- 在 Redis 客户端初始化时注册统一的
CacheKeyPrefixer中间件,自动为所有 key 加前缀 - 配合 Redis ACL,限制应用账号只能访问以
tenant:开头的 key,这是第二道防线 - 如果用的是集群模式,确保前缀一致(如都用
tenant:{org-456}:xxx),避免哈希槽错位
Weaviate 必须启用 Tenant-aware Collections,API Key 不够用
Weaviate 默认单 Collection 共享所有 Embedding,仅靠中间件拦截请求或 API Key 鉴权,完全无法阻止跨租户向量泄露。A 租户写入的文档,B 租户调用 nearText 就可能命中。
必须创建开启 multi-tenancy 的 Collection:
client.schema.createClass(&class{
Class: "Document",
MultiTenancyConfig: &models.MultiTenancyConfig{Enabled: true},
})
每次操作都显式传 tenant: "org-456":
-
createObject时加Tenant: "org-456" -
nearText查询时也必须带Tenant: "org-456" - 禁用 multi-tenancy 的 Weaviate 集群不可用于多租户场景,无论中间件怎么拦都没用
行级隔离不能只靠中间件 + 全局作用域,必须数据库 RLS 补刀
像 Iris 这类框架没有 Laravel 那样的 Eloquent 全局作用域,开发者容易在手写 SQL 或原生查询时漏加 WHERE tenant_id = ?。测试环境数据少,串号问题极难暴露;上线后某次报表导出,突然拉出全库订单。
最危险的是“信任中间件已设好上下文”,然后在 DAO 层直接拼 SQL,或用 ORM 原生查询绕过过滤逻辑。
- PostgreSQL / MySQL 8.0+ 必须开启 Row-Level Security (RLS),定义策略强制所有查询带上
tenant_id - 即使应用层写了条件,RLS 是兜底:没带或写错,数据库直接返回空或报错
- 注意 RLS 策略要覆盖所有角色(包括应用连接用户),且需
ENABLE ROW LEVEL SECURITY显式开启表级开关
最容易被忽略的是网络层和数据层之间的盲区:中间件只管进来的 HTTP 请求,但挡不住内部服务直连数据库/Redis/Weaviate。所有下游组件调用必须走封装后的 Client,而 Client 内部必须从 isolatedCtx 提取 tenant_id,不是从中间件设置的 context 或闭包变量里拿。











