beego中租户id必须从子域名、路径前缀或jwt等可信源提取,并在prepare()中校验绑定;orm需封装tenantorm自动追加tenant_id过滤;缓存、session、文件路径等均须强制添加租户前缀,定时任务等边缘通道需手动注入上下文。

Beego 框架本身不内置多租户支持,必须靠手动注入租户上下文 + 全局数据过滤 + 中间件拦截来实现,否则所有查询都可能跨租户泄露数据。
如何在 Beego 请求生命周期中安全提取租户标识
租户 ID 不能依赖前端传入的 tenant_id 参数或 header(易伪造),必须绑定到可信入口点。常见做法是:从子域名(如 acme.example.com)、路径前缀(如 /t/acme/)或 JWT claim 中解析,并在 Prepare() 方法里完成校验与绑定。
- 子域名方案需提前配置 DNS 泛解析,且 Beego 的
AppConfig.String("httpport")不直接暴露 Host,要用this.Ctx.Request.Host解析后查租户注册表(如 Redis 缓存的tenant:domain:acme.example.com) - 路径前缀方案需在路由注册时启用
beego.Router("/t/:tenantid/:splat", &MainController{}),并在Prepare()中校验this.Ctx.Input.Param(":tenantid")是否合法、是否已激活 - JWT 方案要求所有请求带
Authorization: Bearer xxx,用jwt.Parse验签后取claims["tenant_id"],失败则直接this.Abort("401")
如何避免 ORM 查询绕过租户隔离
Beego 的 orm.QuerySeter 默认不做租户过滤,一旦漏写 .Filter("tenant_id", tid) 就会读到其他租户数据。最稳妥的做法是封装一个带自动过滤的 TenantOrm 工具类,所有模型操作都走它。
- 对每个模型结构体加
tenant_id int64 `orm:"column(tenant_id)"`字段,并确保数据库所有业务表都有该字段和索引 - 重写
Read()、ReadOrCreate()、QueryTable()等方法,在内部自动追加.Filter("tenant_id", t.tid) - 禁用裸 SQL 查询;若必须用
Raw(),强制要求参数化并显式拼入AND tenant_id = ?,否则 CI 流水线应拦截 - 在测试阶段用 SQL 日志检查:开启
orm.Debug = true,确认每条 SELECT 都含tenant_id = N
如何让 Beego 的缓存层也按租户隔离
Beego 的 cache 模块(如 redis 或 memory 驱动)默认全局共享 key 空间,cache.Put("user_123", data, 3600) 会被所有租户看到。必须在 key 前缀强制加入租户上下文。
- 不要直接调用
cache.Put(),统一走封装函数TenantCachePut(tid int64, key string, val interface{}, expire int64),内部拼成fmt.Sprintf("t%d:%s", tid, key) - Session 存储也要隔离:配置
session.providerconfig为{"key": "beego-session-id", "cookiepath": "/t/{tenant_id}/"},或改用自定义 SessionProvider,在SessionStart()中注入租户 ID 到 session key - 模板缓存(
beego.BConfig.WebConfig.TemplateCache)无需隔离,它是编译态资源;但动态渲染内容(如租户定制 CSS)必须走 TenantCache
Beego 多租户下最容易被忽略的三个漏洞点
很多团队在上线后才发现数据混杂,问题往往出在非主流程的“边缘通道”:
- 定时任务(
beego.BeeLogger.Info()启动的 goroutine)没携带租户上下文,导致批量清理、报表生成等任务误操作全量数据 - WebHook 回调接口未做租户绑定校验,攻击者伪造回调地址可触发任意租户的支付状态更新
- 文件上传路径未按租户分目录,如
static/uploads/avatar.png被多个租户覆盖,或 COS/CDN URL 未加租户前缀导致越权访问
这些地方没有标准中间件能覆盖,必须在每个 handler 入口手动调用 BindTenantContext() 并做防御性断言,比如 if ctx.TenantID == 0 { this.Abort("400") }。











