tenant_id字段隔离必须显式注入且全程不可绕过,因gorm的preload、count、raw等均绕过scopes;postgresql需用sync.map缓存租户专属*sql.db并设search_path;文件路径、配置key、权限模型均须按租户显式隔离。

tenant_id 字段隔离必须显式注入,不能依赖 GORM Scopes
GORM 的 Preload、Count、Raw、Joins、Unscoped 全部绕过自定义 Scopes。你写一个 ByTenant(tenantID) Scope,db.Scopes(ByTenant(tenantID)).Preload("Orders").Find(&u) 仍然会加载所有租户的订单——因为 Preload 内部新建了独立 *gorm.DB 实例,不继承外层 Scope。
- 所有 DAO 方法签名必须显式接收
tenantID string参数,禁止从context.Context取值(异步任务、定时器、消息消费中ctx.Value易为空或 panic) -
Preload必须手动传条件:db.Preload("Orders", func(db <em>gorm.DB) </em>gorm.DB { return db.Where("tenant_id = ?", tenantID) }) -
Count不能写db.Model(&User{}).Count(&count),必须加过滤:db.Where("tenant_id = ?", tenantID).Model(&User{}).Count(&count) - 软删除字段如
deleted_at必须和tenant_id同级约束:Where("tenant_id = ? AND deleted_at IS NULL", tenantID),否则Unscoped().Where("id = ?", x)直接越权 - 每张业务表的
tenant_id字段必须建联合索引,例如(tenant_id, created_at),否则分页查询延迟秒级起步
search_path 隔离必须配合 sync.Map 缓存租户专属 *sql.DB
PostgreSQL 的 search_path 是 session 级变量,database/sql 连接池不保证复用同一物理连接。你在中间件里执行 db.Exec("SET search_path TO tenant_abc"),连接归还后状态丢失,下个请求取到的连接仍是默认 public schema,极易串租户。
- 用
sync.Map缓存tenantID → *sql.DB,key 推荐用子域名(如acme),不是 UUID - 首次访问时调用
sql.Open,立刻执行db.Exec("SET search<em>path TO " + safeSchemaName)</em>,safeSchemaName必须白名单校验(正则^[a-z][a-z0-9]{2,30}$) - 每个租户专属
<em>sql.DB</em>必须调用db.SetMaxOpenConns(5)和db.SetConnMaxLifetime(5 time.Minute),否则 1000 租户 × 默认 100 连接 = PostgreSQL 直接 OOM - 禁止手写
"tenant_abc.users"拼表名,它破坏 GORM 钩子(如BeforeCreate)、难测、易 SQL 注入
Casbin 权限校验必须升级为四元组模型
Casbin 默认 RBAC 模型 rbac_model.conf 只定义 r = sub, obj, act,所有租户共享策略空间。一旦 alice 在租户 A 被赋予权限,她可能凭同一角色访问租户 B 的资源。
- 修改模型为四元组:
r = sub, tenant, obj, act,并在[matchers]中强制校验r.tenant == p.tenant - 调用时必须传四参数:
e.Enforce("alice", "tenant-abc", "/api/users", "read"),不能用三参数旧接口 - 策略行必须显式带租户:
p, admin, tenant-abc, /api/users, read;角色继承也得带租户:g, alice, admin, tenant-abc - 不要为每个租户启一个
Enforcer实例——内存爆炸、策略同步难;用单实例 +FilteredAdapter加载租户策略子集更可控
文件系统与配置中心操作必须按租户重写路径和 key
Go 标准库完全不感知租户。os.Open、filepath.Join、viper.Get 等 API 均无租户上下文,直接拼接或复用全局句柄会导致租户 A 读到租户 B 的数据。
- 租户路径必须以
tenantID为第一级目录,例如/data/tenant_abc/uploads/xxx.png - 所有路径构造前必须调用
filepath.Clean(),再比对是否仍以/data/tenant_abc/开头,拦截../路径遍历 - 上传文件名必须重写为 UUID 或哈希,禁止保留原始名(含恶意字符或
..片段) -
os.Stat和os.Chmod前必须先调用validateTenantPath(tenantID, path) - 配置中心客户端(如 etcd)必须按租户隔离 watch 和缓存,
viper是全局单例,不能直接复用
真正安全的多租户不是靠“框架自动兜底”,而是把 tenant_id 或 search_path 作为刚性组成部分,从连接建立、SQL 构建、缓存键名、文件路径到权限策略,全部显式绑定——漏掉任何一个环节,就是 P0 数据泄露。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











