租户标识必须在中间件一次性提取并存入 gin context,通过 c.set("tenant_id", tid) 安全传递;禁止 handler 临时解析 host 或 jwt,避免重复校验与并发串租;host 解析需先 splithostport 再拆前缀,本地请求应跳过;路由不可动态分租户组,须统一路径+显式上下文;db、缓存、限流、mq 等所有外部依赖均需显式注入 tenant_id;数据库隔离须结合 rls(pg)或连接池分片(mysql)+ where 条件兜底;casbin 权限控制必须将 tenant_id 嵌入 obj 或升级四元组,策略加载需带 tenant 过滤。

租户标识必须在中间件里一次性提取并存入 context
租户 ID 不能靠 handler 里临时解析 Host 或 JWT,否则每个函数都得重复校验、容错、取值,极易漏掉或不一致。Gin 的 c.Set("tenant_id", tid) 是唯一安全的传递方式;全局变量、闭包捕获、函数参数透传都会在并发下串租户。
常见错误现象:本地调试时 localhost:3000 崩溃,或测试用 curl -H "Host: t1.example.com:8080" 提取出 t1.example.com:8080 当作租户名——因为没先调 net.SplitHostPort(r.Host) 拆端口。
- 优先级建议:JWT payload 中的
tenant_id> Host 头(需白名单校验) >X-Tenant-IDheader(仅限内网可信链路) - Host 解析务必先用
net.SplitHostPort取 host,再strings.SplitN(host, ".", 2)拆前缀,主域名如"example.com"必须定义为常量 - 本地请求(
localhost、127.0.0.1)应跳过提取,走默认租户或直接c.AbortWithStatusJSON(400, ...) - 提取后立刻校验租户有效性(比如查缓存确认是否启用),失败就中止,别留到 handler 里再查
Gin 路由不能动态按租户分组,必须共用路径 + 显式携带 tenant 上下文
router.Group(tenantID + "/api") 这类写法在 Gin 里完全无效——Group() 是构建期静态注册,r.Run() 后路由树就冻结了,不可能按每次请求的 Host 实时生成新分支。
真正可行的是“一套路由、多套上下文”:所有租户都走 /api/users,但每个 handler 开头就调 c.MustGet("tenant_id"),然后把 tenant_id 显式传给下游:
- DB 查询必须带
tenant_id条件,禁止裸表操作如db.Table("users"),一律封装成tenantDB.User().List() - 缓存键必须含 tenant,例如
"tenant:" + tenantID + ":user:" + userID,硬拼比序列化 struct 更可靠 -
c.Param("id")和c.Query("page")不受影响,但它们只是输入,不自动参与隔离——你得手动把 tenant_id 和它们一起喂给业务逻辑 - 限流器、消息队列 topic、文件存储路径等所有外部依赖,都要显式注入 tenant_id
数据库隔离不能只靠 WHERE tenant_id = ?,必须连接粒度 + 策略兜底
应用层加 WHERE tenant_id = ? 是最脆弱的一道防线。ORM 自动生成 SQL、开发者手写原生查询、甚至 GORM 的 Session() 忘传 context,都会导致条件丢失,数据裸奔。
PostgreSQL 用户应直接上 Row-Level Security(RLS):用 SET LOCAL app.tenant_id = 'xxx' 配合策略函数;MySQL 则必须结合连接池分片(如 tenant_pool_map[tid%4])+ 所有 DB 调用强制绑定 tenant context。
- GORM v2+ 推荐用
db.Session(&gorm.Session{Context: ctx})封装,但前提是每个db.调用都显式传这个 session,不能依赖默认实例 - 禁止在 model 层硬编码
tenant_id字段,而是通过DefaultTableNameHandler动态注入 schema 名(共享库独立 Schema 场景)或通过 RLS 表达式(共享表场景) - 跨租户统计类需求,必须走专用分析库或离线任务,绝不允许在主库上漏掉
tenant_id条件跑聚合
Casbin 权限控制必须把 tenant_id 嵌入 obj 或扩展四元组
Enforce() 总是返回 false,90% 是因为 tenant_id 没塞进 Casbin 的三元组。Casbin 默认只认 sub、obj、act,不会自动从 Gin context 里捞 tenant_id。
最简单可靠的方案是把 tenant_id 作为 obj 的前缀,比如 "tenant-a:order:123",对应策略写成 p, user1, tenant-a:order:*, read。若必须区分 sub 所属租户和操作目标租户,则得升级为四元组:
- 模型文件改用
[request_definition]四字段声明:r = sub, tenant, obj, act - 实现自定义
MatchFunc,从 Gin context 提取 tenant_id 并填入 request 第二位 - 加载策略时必须用支持过滤的 adapter(如
gorm-adapter),调LoadPolicyWithFilter(&casbin.Filter{V3: []string{"tenant-a"}}),而非内存过滤 -
GetPermissionsForUser()返回脏数据?说明策略里的obj没带租户前缀,或g行角色继承也漏了 tenant(如g, u1001, admin-tenant-a)











