tenant_id字段隔离易出P0泄露,因所有租户数据共存一表,漏加WHERE tenant_id = ?即裸查全量;GORM的Preload、Count、Raw等操作天然绕过Scopes,且软删除未与tenant_id同级约束时Unscoped可直接越权。

tenant_id 字段隔离为什么容易出 P0 数据泄露
因为所有租户数据混在同一张表里,漏加 WHERE tenant_id = ? 就等于裸查全量数据——而 GORM 的多数操作天然绕过你写的 Scopes。
-
Preload不自动继承 Scopes,必须显式传入db.Scopes(ByTenant(tenantID)).Preload("Orders") -
Count、FirstOrInit、Joins、Raw查询完全不感知租户上下文,写错一次就是跨租户读取 - 软删除字段(如
deleted_at)若没和tenant_id同级约束,Unscoped().Where()可直接绕过租户过滤 - 任何直连 DB 的路径(日志写入、后台任务、第三方库调用)都得手动补
tenant_id,漏一个点就崩
PostgreSQL schema 隔离怎么避免连接池爆炸
别为每个租户新建 *sql.DB 实例,也别在每次查询前拼 SET search_path TO tenant_abc ——前者耗内存,后者破坏连接复用。
- 用
sync.Map缓存tenantID → *sql.DB,key 推荐用子域名(如acme),不是 UUID - 每个
*sql.DB必须调用db.SetMaxOpenConns(5)和db.SetConnMaxLifetime(5 * time.Minute),否则 1000 租户 × 默认 100 连接 = PostgreSQL 直接拒绝新连接 - 初始化时执行
SET search_path TO tenant_acme,后续所有查询自动生效,无需改 SQL 或 ORM 表名 - 禁止手写
"tenant_acme.users"这类拼接表名,它破坏 ORM 抽象、难测、易注入
GORM 中如何安全注入 tenant_id 而不依赖 context.Value
把 tenantID 塞进 context.Context 再一路透传到 DAO 层,等于把隔离责任甩给每个开发者——异步任务、定时器、消息消费根本拿不到,一漏就是越权。
- DAO 层所有函数签名必须显式接收
tenantID string,例如:func (d *UserDAO) GetByID(tenantID string, id uint) (*User, error) - 禁用
db.Where().Find()这类裸调,封装成d.db.WithTenant(tenantID).Where(...)方法 - 如果用 GORM Scopes,确保
BeforeFind回调注册在全局*gorm.DB上,且不被Session或Unscoped绕过 - 测试时强制覆盖所有 DAO 路径,验证每条 SQL 是否含
tenant_id = ?条件
MySQL 下为什么不能照搬 PostgreSQL 的 schema 方案
MySQL 没有 search_path,也没有原生的 schema 切换机制。硬套 PostgreSQL 的思路只会让连接池失控、运维成本飙升。
立即学习“go语言免费学习笔记(深入)”;
- 分库方案(
dbname=tenant_acme)是唯一可行替代,但每个租户需独立*sql.DB,连接无法复用 - 1000 租户 ≈ 1000 个连接池,每个默认 100 连接 → 10 万 TCP 连接,MySQL 很快报
Too many connections - 迁移工具(如
golang-migrate)不支持动态 dbname,得自己写脚本遍历租户列表挨个执行 - 除非用 ProxySQL 或 Vitess 做路由层,否则 MySQL 场景下
tenant_id字段隔离仍是更务实的选择
真正卡住落地的不是技术选型,而是连接池配置粒度和测试覆盖方式——sync.Map 缓存 *sql.DB 时没设 MaxOpenConns,或 DAO 测试只跑 happy path,漏掉 Preload 和 Raw 路径,上线后第一周就会触发数据越界告警。


















