租户标识必须在最外层中间件提取校验,从x-tenant-id、子域名或jwt claim获取并查库验证有效性;每个请求须绑定独立*sql.db实例,通过sync.map缓存;handler中必须用c.get("tenant_db")显式获取;casbin策略需嵌入租户id并按租户加载。

租户标识必须在路由之前提取和校验
不能把租户识别逻辑放在某个具体路由 handler 里,否则 /users、/orders 等所有业务路径都得重复写一遍解析和白名单检查。必须在最外层中间件(比如 echo.MiddlewareFunc)中完成:从 X-Tenant-ID header、子域名或 JWT claim 中取值,查数据库确认租户存在且未被禁用,失败立即返回 echo.NewHTTPError(400, "invalid tenant")。
常见错误是只做非空判断:c.Request().Header.Get("X-Tenant-ID") != "",但攻击者可伪造任意字符串;正确做法是查 tenants 表的 status = 'active' 且 subdomain 或 tenant_code 字段匹配。
- 子域名解析别用正则硬匹配(如
^([a-z0-9]+)\.example\.com$),应取c.Request().Host,strings.Split(host, ".")后取首段,再查库 - JWT 场景下,租户 ID 必须来自
token.Claims["tenant_id"]等可信字段,不能从 query 或 body 解析 - 绝对不要用路由参数(如
/t/:tenant_id/users)作为唯一依据——路径遍历或中间件跳过会导致绕过
每个请求必须绑定独立的 *sql.DB 实例
框架中间件里不能声明全局 var tenantDB *sql.DB,也不能在闭包里复用连接。正确路径是:根据租户 ID 查 sync.Map 缓存,命中则返回已有 *sql.DB;未命中则调用 sql.Open(driver, dsn) 创建新实例,存入缓存后再注入 context。
示例关键操作:
db, ok := tenantDBCache.Load(tenantID)
if !ok {
db, _ = sql.Open("mysql", buildDSN(tenantID))
db.SetMaxOpenConns(20)
tenantDBCache.Store(tenantID, db)
}
c.Set("tenant_db", db) // 注入到 echo.Context
-
*sql.DB是连接池抽象,不是单连接,跨租户复用会导致SetMaxOpenConns冲突、事务泄漏、甚至 driver.Valuer 行为污染 -
db.Close()绝不能在中间件或 handler 里调用——那是关整个池,应在服务 shutdown 阶段统一关闭 - 缓存建议用带 TTL 的方案(如
github.com/bluele/gcache),避免租户停用后连接长期滞留
Handler 中必须显式从 context 获取 DB,不可传参或全局查找
业务 handler 里不能通过函数参数传 DB,也不能去全局 map 查。必须调用 c.Get("tenant_db"),然后做类型断言:db := c.Get("tenant_db").(*sql.DB)。这是唯一能确保当前请求拿到自己租户 DB 的方式。
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
容易踩的坑:
- DAO 层直接依赖全局
var db *sql.DB—— 这会把所有租户流量打到同一个库 - 在 handler 里重复查租户库或重建 DSN —— 中间件已查过一次,结果应复用
- 忘记类型断言或 panic 捕获,导致 500 错误而非 400
Casbin 权限校验需嵌入租户上下文
如果用 Casbin 做 RBAC,enforcer.Enforce() 默认只认 sub, obj, act 三元组。若不把租户 ID 塞进 obj(如 "tenant-a:order:123")或扩展为四元组,Enforce 必然返回 false —— 因为策略里写的都是带租户前缀的 obj,而你传进去的是裸名。
实操要点:
- 策略文件里写
p, user1, tenant-a:order:*, read,代码里调用enforcer.Enforce("user1", "tenant-a:order:123", "read") - 加载策略时用
adapter.LoadPolicyWithFilter(&casbin.Filter{V3: []string{"tenant-a"}}),避免全量加载 - 每次 HTTP 请求进来,先
enforcer.ClearPolicy()再按租户加载,否则旧租户策略残留
最易忽略的一点:租户 DB 切换和 Casbin 策略加载必须同步,且都在中间件阶段完成;任何延迟到 DAO 层才取租户 ID 的设计,都会导致连接池混乱和权限失效。










