5.8 多租户隔离
概述
多租户是硬隔离:9 张租户级表的 tenant_id 过滤与盖章由全局钩子强制执行,业务代码零感知、不可绕过。租户状态与过期由 Tenant 中间件每请求校验(60s 缓存)。
数据模型
text
goframe_tenant:code(T+4位ID,Account 生成)、status(1 正常/2 禁用)、
expire_time(nil=永不过期)、max_user_num 等
9 张租户级表(带 tenant_id 列,hooks.tenantTables 登记):
goframe_user / role / dept / level / position / article / category / notice / link
平台级(无租户过滤):其余 16 张表读侧隔离(不可绕过)
text
dao.Ctx(hooks.ApplyCtx):非平台上下文自动追加
WHERE tenant_id IN (0, 当前租户) ← 0 = 平台共享行
超管(ctx tenant_id=0):不加过滤,全量可见
写侧禁止手写该条件——dao.Ctx 是唯一查询入口写侧归属(insertHook 盖章规则)
text
租户用户插入未显式指定 tenant_id → 自动填自身(防逃逸)
超管未显式指定 → 填 tenant.defaultId(默认 1)
显式指定具体值 → 从显式值
显式 0 → 创建平台共享行(如全局字典/参数)
业务取归属租户统一 consts.GetTenantID(ctx)(超管归默认租户);
CtxTenantID(ctx) 原始值仅供租户设施使用。请求级校验(Tenant 中间件)
text
位于 JWT 之后:
* 公开路径放行
* ctx tenant_id=0(平台/超管)直接放行
* 其余加载租户摘要(缓存 easygoadmin:tenant:info:{id},TTL 60s):
- status != 1 → code=1006 租户已禁用
- expire_time 早于现在 → code=1005 租户已过期
LoadTenant 语义:租户不存在 → 按禁用处理(失效关闭);
数据库基础错误 → 返回 nil,中间件放行(避免存储抖动导致全站不可用)租户状态机
text
Account 开户
│
▼
┌─────────┐ Status(2) ┌─────────┐
│ 1 正常 │─────────────▶│ 2 禁用 │
└─────────┘ └─────────┘
│ │
│ expire_time 到期 │ Status(1)
▼ │
过期(1005 拒绝访问)◀──────────┘
(expire_time nil = 永不过期)状态/过期变更后 tenant:info:{id} 缓存由 tenant 模块 AfterUpdate clearCache 钩子清理。
租户开户(tenant.Account,POST /api/tenant/account)
text
创建租户 + 初始管理员:事务内
1) 建租户(code 按 T+ID 规则生成)
2) 建管理员用户(bcrypt 事务外预计算)
3) 分配角色
max_user_num 上限校验(1007)在 user.Add 生效(checkTenantUserMax)
事务纪律:事务内不做 HTTP/文件 IO,bcrypt 等 CPU 计算事务外预计算配套接口:GET /api/tenant/users/:tenantId 查询租户用户(sys:tenant:users)。
安全审计要点
租户红线
- 业务代码禁止
g.DB()直查业务表 - 业务代码禁止手写 del_flag / tenant_id 条件
- 新增租户级表必须登记 hooks.tenantTables(漏登=跨租户泄漏)
- 唯一预检范围必须等于唯一索引范围(含 tenant_id 索引须 Group:"tenant_id")
- 平台级全量可见是设计预期,管理端点须收紧 permission
排障
| 症状 | 原因 |
|---|---|
| 租户看到别家数据 | tenantTables 漏登记该表;或业务绕过 dao.Ctx |
| 租户看不到平台共享行 | 业务手写了 tenant_id = ?(应交给 dao.Ctx 的 IN(0,x)) |
| 禁用租户仍能访问 | 等 60s 缓存过期,或 DEL easygoadmin:tenant:info:{id} |
| 新增报租户用户数上限 | 1007——超过 max_user_num |
| 超管新增数据归属不对 | 显式指定 tenantId 或调 tenant.defaultId 配置 |