签入版本
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
# 5.8 多租户隔离
|
||||
|
||||
:::tip 概述
|
||||
多租户是硬隔离: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)。
|
||||
|
||||
## 安全审计要点
|
||||
|
||||
::: warning 租户红线
|
||||
1. 业务代码禁止 `g.DB()` 直查业务表
|
||||
2. 业务代码禁止手写 del_flag / tenant_id 条件
|
||||
3. **新增租户级表必须登记 hooks.tenantTables(漏登=跨租户泄漏)**
|
||||
4. 唯一预检范围必须等于唯一索引范围(含 tenant_id 索引须 Group:"tenant_id")
|
||||
5. 平台级全量可见是设计预期,管理端点须收紧 permission
|
||||
:::
|
||||
|
||||
## 排障
|
||||
|
||||
| 症状 | 原因 |
|
||||
|------|------|
|
||||
| 租户看到别家数据 | tenantTables 漏登记该表;或业务绕过 dao.Ctx |
|
||||
| 租户看不到平台共享行 | 业务手写了 `tenant_id = ?`(应交给 dao.Ctx 的 IN(0,x)) |
|
||||
| 禁用租户仍能访问 | 等 60s 缓存过期,或 DEL `easygoadmin:tenant:info:{id}` |
|
||||
| 新增报租户用户数上限 | 1007——超过 max_user_num |
|
||||
| 超管新增数据归属不对 | 显式指定 tenantId 或调 tenant.defaultId 配置 |
|
||||
Reference in New Issue
Block a user