Revel中租户识别必须从Host头或请求上下文提取:需在自定义拦截器中解析c.Request.Host或X-Tenant-ID/JWT payload,存入c.Params.Routes["tenant"]或context.WithValue;严禁依赖Session;GORM连接池须按租户预热并独立配置,模型操作必须显式使用对应租户DB实例,共享表模式下必须启用数据库行级安全(RLS)兜底。

Revel 中租户识别必须从 Host 头或请求上下文提取
Revel 没有内置多租户支持,租户标识得你自己从 Host 请求头、X-Tenant-ID 请求头,或者 JWT payload 里解析出来。不能靠 session 或 cookie 自动带,因为中间件执行顺序和上下文生命周期容易出错。常见错误是把租户 ID 存在 revel.Controller.Session 里再取,结果异步任务或定时任务里拿不到——Session 是 HTTP 生命周期绑定的。
推荐做法是:在自定义拦截器中解析 c.Request.Host(如 tenant1.example.com → 提取 tenant1),然后存入 c.Params.Routes["tenant"] 或用 context.WithValue 注入到请求上下文。后续所有 DB 操作都基于这个值路由。
GORM 连接池需按租户预热,不能运行时临时 Dial
Revel 启动阶段就要初始化多个 GORM *gorm.DB 实例,比如 DBMain(元数据)、DBTenantA、DBTenantB。别在每次请求里调用 gorm.Open(),否则连接数爆炸、TLS 握手耗时飙升,还可能触发数据库连接拒绝。
关键点:
- 每个租户连接池应配置独立的
gorm.Config,尤其是PrepareStmt: true和连接池参数(MaxOpenConns、MaxIdleConns) - 租户库名/DSN 必须从配置中心或环境变量加载,不能硬编码;支持热更新(如监听 etcd 变更)
- 避免用
db.Session(&session)伪造租户上下文——它不改变底层连接,只影响 Hooks 和 Scope
模型层必须显式绑定租户连接,不能依赖全局 DB
你不能让所有 Order、Product 模型都用同一个 DBMain。GORM 的 Scopes 或 Where("tenant_id = ?", tid) 是共享表模式的补救手段,不是隔离方案。一旦漏写或用了 Raw()、Transaction(),就直接越界。
正确姿势是让每个租户的模型操作明确使用对应连接:
// 在 controller 里 tenantDB := getTenantDB(c.Params.Routes["tenant"]) // 从 map 或 registry 获取 var orders []Order tenantDB.Find(&orders)
更稳妥的做法是封装一个 TenantDB 类型,内嵌 *gorm.DB 并强制携带 tenantID 字段,所有查询方法都走它,杜绝误用全局实例。
共享表模式下必须启用 RLS 或自动 WHERE 注入
如果业务决定用共享库+tenant_id 字段(比如租户量超 2000),仅靠应用层过滤远远不够。Revel 拦截器可以统一注入 Where("tenant_id = ?", tid) 到 GORM 的 Scope,但 Raw()、Count()、关联预加载(Preload)仍可能绕过。
真正防漏的底线是数据库行级安全(RLS):
- PostgreSQL:在
orders表上建策略CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.tenant_id', true)::uuid) - MySQL 8.0+:用
SQL SECURITY DEFINER视图封装,或升级到 8.0.30+ 的 RLS 支持 - 务必配合
SET app.tenant_id = 'xxx'在连接初始化时设置,且每个请求复用连接前重置
这层数据库守门员不能省——人写的代码会漏,SQL 解析器不会。











