← 返回博客
go2026-09-12 22:01:342 分钟 · 516 0

Gin 多租户架构:三种隔离方案落地

对比独立库、独立 schema、共享表行级三种多租户隔离方案,重点讲行级方案下用中间件解析租户、GORM 作用域自动注入 tenant_id。

#gin#多租户#隔离

一套服务要服务多个客户(公司、团队),每个客户的数据必须隔离,串了就是安全事故。多租户有三种隔离力度,按成本和合规要求选。这一篇把三种都讲清,重点给最常用的行级方案落地代码。

1. 三种方案对比

独立库:每租户一个数据库,隔离最强、成本最高。独立 schema:同库不同 schema,折中。共享表行级:所有租户一张表,每行带 tenant_id,成本最低、最容易出错。绝大多数 SaaS 用行级。

2. 行级:中间件解析租户

请求带租户标识(子域名、header、JWT claim),中间件解析后写进 context:

func Tenant() gin.HandlerFunc {
    return func(c *gin.Context) {
        tid := c.GetHeader("X-Tenant-Id")
        if tid == "" {
            c.JSON(400, gin.H{"error": "缺少租户标识"})
            c.Abort()
            return
        }
        ctx := context.WithValue(c.Request.Context(), "tid", tid)
        c.Request = c.Request.WithContext(ctx)
        c.Next()
    }
}

3. GORM 作用域自动带 tenant_id

每条查询手动加 Where("tenant_id = ?") 容易漏。用 GORM scope 统一注入:

func TenantScope(ctx context.Context) func(*gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        if tid, ok := ctx.Value("tid").(string); ok {
            return db.Where("tenant_id = ?", tid)
        }
        return db
    }
}
// 用法
db.Scopes(TenantScope(ctx)).Find(&orders)

新建也自动填 tenant_id,从源头杜绝串租户。

4. 独立 schema 方案

同库按租户切 schema,查询前 Set:

db.Exec("SET search_path TO ?", "tenant_"+tid)

PostgreSQL 的 schema 天然适合。切换要在连接池按租户取专用连接,否则串到别的租户。

5. 独立库方案

租户映射到独立 DSN,连接池按租户路由:

func dbForTenant(tid string) *gorm.DB {
    dsn, ok := tenantDSN[tid]
    if !ok {
        return nil
    }
    return connect(dsn) // 带连接池缓存,别每次新建
}

隔离最彻底,适合合规要求高的金融、医疗客户。代价是连接数多、迁移要逐库跑。

6. 上线清单

租户解析必须在最外层中间件,任何请求都过。行级方案所有读写都带 tenant_id,作用域统一注入别手写。租户标识来源要做校验,不能让用户伪造访问别人数据。跨租户统计、运维操作走独立的高权限通道,不混在普通接口。独立库/schema 方案迁移脚本要逐租户执行并校验。

下一篇讲集成测试,用真实依赖替掉 mock,把上面这些都测住。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/192