字段级隔离必须强制注入tenant_id,不能依赖orm scopes;postgresql schema隔离需为每个租户缓存独立*sql.db实例并设置search_path;文件系统和配置中心操作也须按租户隔离路径与key。

字段级隔离必须强制注入 tenant_id,不能依赖 ORM Scopes
字段级隔离(即所有租户共享表、靠 tenant_id 字段过滤)只适合中小规模 SaaS,但即便如此,也极易因一次漏写条件导致全量数据泄露。GORM 的 Scopes 看似方便,实际非常脆弱:
-
Raw()、Joins()、Count()、Unscoped()会直接绕过 Scopes - 软删除字段(如
deleted_at)若未与tenant_id同级约束,Unscoped().Where("id = ?", x)就能跨租户读取 - Preload 关联查询默认不继承主查询的租户条件,
db.Preload("Orders").Find(&u)可能加载其他租户的订单
正确做法是:所有 DAO 方法签名必须显式接收 tenantID string 参数,并在每个 SQL 构建处手动拼接 WHERE tenant_id = ?;禁止裸调用 db.Where(),封装成带租户校验的 FindWithTenant() 或类似方法。
PostgreSQL schema 隔离需为每个租户缓存独立 *sql.DB 实例
PostgreSQL 的 search_path 是 session 级设置,无法跨连接复用——这意味着你不能靠中间件里执行 SET search_path TO tenant_abc 就完事。连接池归还后,下次取到的连接仍是默认 public schema。
- 必须用
sync.Map缓存tenantID → *sql.DB,首次访问时初始化专属实例 - DSN 中追加 URL 编码后的
options=-c+search_path%3Dtenant_abc,确保连接建立即生效 - 每个租户的
*sql.DB必须调用SetMaxOpenConns(5)等限流,否则 1000 租户 × 默认 100 连接 = 10 万连接,直接打崩 PostgreSQL - 不要试图用 GORM
Session()动态换 schema——它不控制底层连接绑定,事务中可能跨连接失效
文件系统操作必须校验路径归属并重写文件名
Go 标准库完全不感知租户,os.Open、os.Stat、filepath.Join 等 API 均无租户上下文。直接拼接路径或复用全局 *os.File 实例,会导致租户 A 读到租户 B 的文件。
- 租户路径必须以
tenantID为第一级目录,例如/data/tenant_abc/uploads/xxx.png - 所有路径构造前必须调用
filepath.Clean(),再比对是否仍以/data/tenant_abc/开头,拦截../路径遍历 - 上传文件名必须重写为 UUID 或哈希,禁止保留原始名(含恶意字符或
..片段) -
os.Stat和os.Chmod前必须先调用validateTenantPath(tenantID, path),否则元数据操作可能越权
配置中心客户端必须按租户隔离 watch 和缓存
viper 是全局单例,etcd client 的 Watch() 接口不支持租户过滤。多个租户共用同一 watch channel 或内存 map,会导致配置变更“串包”——租户 A 的配置更新触发租户 B 的回调。
- etcd key 必须嵌入租户前缀,例如
/tenant/{tenant_id}/config/app.yaml,而非在 value 中塞租户字段 - 每个租户应持有独立
TenantConfigClient实例,内嵌*clientv3.Client和tenantID - watch goroutine 必须按租户分离,每个实例启动自己的
client.Watch(ctx, prefix)并独立消费WatchChan - 缓存也必须分租户,避免用全局
map[string]map[string]interface{}存所有租户配置快照
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











