字段级隔离(tenant_id)在GORM中极易因漏写WHERE条件导致P0数据泄露,不能依赖Scopes或context透传兜底;PostgreSQL schema隔离更安全,但必须为每个租户缓存独立*sql.DB实例并绑定search_path,否则连接复用会直接越权。

字段级隔离(tenant_id)在 GORM 中极易因一次漏写条件导致跨租户数据泄露,不能依赖 Scopes 或 context 透传“兜底”;PostgreSQL schema 隔离更安全,但必须为每个租户缓存独立 *sql.DB 实例并绑定 search_path,否则连接复用会直接越权。
GORM 的 Scopes 为什么根本不可靠
Scopes 是静态封装,对运行时动态租户 ID 无感知;而 GORM 多数高频操作会绕过它:
-
Count()、Raw()、Joins()、Unscoped()全部跳过Scopes,调一次就可能读到全库数据 -
Preload("Orders")不继承主查询的租户条件,加载的是所有租户的订单 - 软删除字段(如
deleted_at)若没和tenant_id同级约束,Unscoped().Where("id = ?", x)可直接绕过租户检查 - 中间件往
context.Context塞tenant_id再手动取值,等于把安全责任甩给每个开发者:异步任务里ctx可能为空,单元测试难覆盖真实 SQL 漏条件场景
字段隔离必须强制 DAO 层显式注入 tenant_id
共享表结构下,唯一可行路径是让租户 ID 成为每个 DAO 方法的硬性参数:
- 所有函数签名必须带
tenantID string,例如FindUserByID(tenantID, id),禁止从ctx.Value获取 - 内部直接拼
Where("tenant_id = ? AND deleted_at IS NULL", tenantID),不依赖任何中间层抽象 -
Preload必须手动加条件:Preload("Orders", func(db *gorm.DB) *gorm.DB { return db.Where("tenant_id = ?", tenantID) }) - 建表时
tenant_id VARCHAR(32) NOT NULL+ 联合索引(如(tenant_id, email)),避免单靠tenant_id索引查得慢 - PostgreSQL 可叠加 RLS 策略作为最后一道防线:
CREATE POLICY tenant_isolation ON users FOR ALL USING (tenant_id = current_setting('app.current_tenant'))
PostgreSQL schema 隔离必须按租户缓存 *sql.DB 实例
search_path 是 session 级设置,连接归还后状态丢失;靠中间件执行 SET search_path TO tenant_abc 是并发不安全的:
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.Map缓存tenantID → *sql.DB,首次访问时初始化专属实例 - DSN 中追加 URL 编码后的
options=-c+search_path%3Dtenant_abc,确保连接建立即生效 - 每个租户的
*sql.DB必须调SetMaxOpenConns(5)和SetConnMaxLifetime(5 * time.Minute),否则 1000 租户 × 默认 100 连接 = 直接打崩 PostgreSQL - 禁止手写
"tenant_abc.users"拼表名——破坏 ORM 抽象,GORM 钩子(如BeforeCreate)失效,迁移工具链断裂 -
GORM v2的Session()或WithContext()不控制底层连接绑定,事务中跨连接切换search_path会失效
MySQL 分库隔离只能靠动态 DSN,且严禁复用连接
MySQL 没有 search_path,也没有原生 schema 权限隔离能力,所谓“schema 隔离”实际只能靠库名区分:
- 每个租户一个 database(如
saas_tenant_123),必须为每个租户生成独立 DSN:"user:pass@tcp(127.0.0.1:3306)/" + tenantID + "?parseTime=true" -
tenantID必须白名单校验(正则^[a-z][a-z0-9_]{2,30}$),防止tenant_123; DROP DATABASE类注入 - 禁用
multiStatements=true,避免一条语句触发多库操作 - 建库操作由运维或迁移工具统一完成,DAO 层只负责查缓存、
sql.Open、错误重试 - 无法复用连接池——每个
*sql.DB实例维护独立连接池,1000 租户 ≈ 1000 个连接池,K8s 下扩缩容风险指数级放大
最易被忽略的是:无论选字段隔离还是 schema 隔离,tenant_id 的提取来源必须可信(JWT payload 或子域名),且全程不可被覆盖或丢失;文件系统、配置中心、缓存 key 也必须同步按租户隔离,否则数据库层再严,上层照样越权。


















