租户id必须通过context.context透传,禁用全局变量或隐式注入;须用私有struct类型作key,从x-tenant-id等安全源提取并校验后注入,所有db查询、缓存、日志均需显式从中获取。

租户 ID 必须作为查询上下文透传,不能依赖全局变量或中间件隐式注入
Go 的 HTTP handler 是无状态的,context.Context 是唯一安全传递租户标识的载体。常见错误是把 tenantID 存在全局 map 或 goroutine-local 变量里,这在并发请求下必然串租户数据。
正确做法是在入口(如 middleware)从请求头(X-Tenant-ID)、子域名或路径中提取租户标识,并用 context.WithValue 注入:
ctx = context.WithValue(r.Context(), tenantKey{}, tenantID)
后续所有 DB 查询、缓存操作、日志打点都必须显式从 ctx.Value(tenantKey{}) 拿值。不要封装成“自动获取租户 ID”的工具函数——那只是把风险藏得更深。
- 租户 Key 类型必须是未导出 struct(如
tenantKey{}),避免与其他库冲突 - 若使用 ORM(如 GORM),需在每个
db.Where()或db.Session()中手动拼接tenant_id = ?条件,GORM 的Scopes不可靠,容易被开发者绕过 - 日志中必须包含
tenant_id字段,否则排查问题时无法区分数据归属
数据库层面强制 tenant_id 为非空且带索引,避免漏加 WHERE 条件导致越权
即使代码层写了 WHERE tenant_id = ?,一旦某次查询漏写(比如用 raw SQL 或跳过 ORM 封装),就直接暴露全量数据。PostgreSQL 或 MySQL 必须对每张多租户表加约束:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
ALTER TABLE orders ADD COLUMN tenant_id VARCHAR(32) NOT NULL;
CREATE INDEX idx_orders_tenant_id ON orders (tenant_id);
更进一步,用行级安全策略(RLS)或视图隔离:
- PostgreSQL:启用 RLS 并创建策略
USING (tenant_id = current_setting('app.tenant_id', true)),再配合SET app.tenant_id = 't123'(需在连接池每次 acquire 后设置) - MySQL:不支持 RLS,只能靠应用层严格校验 + 数据库账号按租户分库/分表授权(例如只给
t123_orders表 SELECT 权限) - 绝对禁止用字符串拼接租户字段名(如
orders_t123),表名硬编码会导致迁移、备份、ORM 映射全部失效
连接池必须按租户隔离或动态切换,不能复用同一连接处理不同租户请求
如果多个租户共享一个数据库连接(尤其用了 connection pool),而连接上残留了前一个租户的 session 设置(如 PostgreSQL 的 current_setting),下一个租户的查询就可能读到错误数据。
两种可行方案:
- 租户粒度连接池:每个租户维护独立
*sql.DB实例(适合租户数少于 100 且连接数可控) - 连接复用 + 显式重置:在
driver.Conn的PrepareContext或ExecContext前,执行SET app.tenant_id = ?(PostgreSQL)或用db.Exec("SELECT ? AS tenant_id", tenantID)清除上次状态(MySQL) - 用 pgxpool 时,务必配置
AfterConnect回调来设置租户上下文,而不是依赖BeforeAcquire
缓存键必须包含 tenant_id,且避免跨租户缓存穿透
Redis key 写成 order:123 是灾难性的。必须是 tenant:t123:order:123 或 order:t123:123。更关键的是缓存穿透防护:当 tenant:t123:order:999 不存在时,不能缓存空结果到 order:999 这种泛化 key 下,否则其他租户查同 ID 订单会误得空响应。
- 所有缓存操作(
Get/Set/Delete)前,先校验 ctx 中tenantID是否存在且非空,否则 panic 或返回 error - 使用分片 Redis 时,确保同一租户的所有 key 落在同一个实例(通过 key hash 规则),否则跨实例事务或批量操作会失败
- 缓存失效策略优先用逻辑删除(如
tenant:t123:order:123:deletedflag),而非直接 DEL,防止瞬间大量回源压垮 DB
WHERE tenant_id = ? 的 raw SQL,或者第 102 行一个没校验 ctx 的缓存封装函数。自动化测试必须覆盖“伪造 X-Tenant-ID 头访问他人数据”这个用例,否则上线即高危。










