PostgreSQL租户隔离必须为每个租户创建独立*sql.DB实例并绑定固定schema,禁用search_path动态切换;GORM需用Scope强制注入tenant_id;MySQL须按租户动态构造DSN分库;租户ID必须白名单校验且从可信上下文提取。

PostgreSQL schema 隔离必须为每个租户配独立 *sql.DB 实例
直接复用同一个 *sql.DB 并靠 SET search_path 切换 schema 是并发不安全的。连接池中的连接会被多个 goroutine 复用,上一个请求设的 search_path 可能被下一个租户继承,导致跨 schema 查询。
真正可行的做法是:按租户 ID 初始化并缓存专属 *sql.DB 实例,每个实例绑定固定 schema:
- 用
sync.Map缓存:tenantDBs sync.Map // key: string, value: *sql.DB - DSN 不带
dbname=后缀(PostgreSQL 下可省略),连接后立即执行db.Exec("SET search_path TO " + safeSchemaName) -
safeSchemaName必须白名单校验,例如正则^[a-z][a-z0-9_]{2,30}$,禁止拼接用户输入 - 每个
*sql.DB调用SetMaxOpenConns(10)、SetConnMaxLifetime(5 * time.Minute),避免连接数爆炸
GORM 行级过滤必须用 Scope 封装 tenant_id 注入
在共享表结构(shared-schema)模式下,靠字段隔离时,tenant_id 条件极易漏加——尤其在 Count、FirstOrInit、Preload 等 GORM 自动构造 SQL 的场景中。
不能靠中间件往 context 里塞值再手动拼 SQL,而应统一用 Scope 强制注入:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 定义函数:
func TenantScope(tenantID string) func(*gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB { return db.Where("tenant_id = ?", tenantID) } } - 所有 DAO 方法必须显式传入
tenantID,调用时链式挂载:db.Scopes(TenantScope(tenantID)).Find(&users) - 软删除字段(如
deleted_at)需与tenant_id同级约束,禁用Unscoped(),或重写BeforeFind回调确保 tenant 过滤不被绕过 - 避免使用
db.Where().Find()这类裸调用,必须走封装后的 scoped 接口
MySQL 分库隔离要动态构造 DSN,且严禁复用连接
MySQL 不支持 search_path,也没有原生 schema 隔离能力,唯一安全路径是分库(dbname=tenant_123)。但直接共用 *sql.DB 实例会因连接复用导致库名错乱。
正确做法是:为每个租户生成独立 DSN,并初始化独立 *sql.DB:
- DSN 拼接示例:
"user:pass@tcp(127.0.0.1:3306)/" + tenantID + "?parseTime=true" - tenantID 同样需白名单校验,防止
tenant_123; DROP DATABASE类注入 - 连接字符串中禁用
multiStatements=true,避免一条语句触发多库操作 - 建库操作由运维或迁移工具统一完成,DAO 层只负责查缓存、
sql.Open、错误降级(失败不 fallback 到默认库)
租户识别必须从请求上下文提取,且不可透传到 SQL 拼接
租户 ID 应来自可信源:子域名(acme.example.com)、JWT claim 或已鉴权的 header(如 X-Tenant-ID),绝不能来自 URL 路径或未校验的 query 参数。
中间件中提取后,必须用私有类型 key 存入 context.Context,并在 DAO 层消费——但仅用于路由或条件注入,绝不用于字符串拼接:
- 定义 key:
type tenantKey struct{},避免context.WithValue(ctx, "tenant_id", ...)这类 string key 冲突 - DAO 方法签名统一为
func (r *Repo) ListUsers(ctx context.Context, ...),内部调用GetTenantID(ctx)获取值 - 禁止出现
"SELECT * FROM " + tenantID + "_users"或fmt.Sprintf("WHERE tenant_id = '%s'", tenantID)—— 这是 SQL 注入温床 - 消息队列、缓存、日志等横向组件也需同步注入租户上下文,但隔离逻辑各自独立(如 Kafka group ID 嵌入 tenantID,RabbitMQ vhost 按 tenantID 创建)
schema 隔离看似只是数据库配置差异,实际牵扯连接生命周期、ORM 行为、迁移策略和监控归因——任何环节试图“省一行代码”绕过租户实例隔离或 scope 封装,都会在租户数增长后暴露为数据越界事故。

















