多租户

AuraBoot 在设计上就是多租户的。同一份部署能承载多个租户,并让它们的数据、配置与凭据保持隔离。多租户由平台的每一层强制落实,不是事后才加上去的。
租户模型
| 概念 | 范围 |
|---|---|
| Tenant | 最顶层的隔离边界;有自己的 users、roles、models 与文件 |
| User | 属于且仅属于一个 tenant;token 中编码了 tenantId |
| Model / page / command | 在 tenant 范围内编写;platform 范围是一个特殊的 "system" tenant |
| Record | 每张动态表上都带 tenant_id |
跨租户访问在设计上不可能发生。一个解析到 tenant A 的请求无法读写 tenant B 的记录,连动态查询 API 都不行。
何时使用
- 承载多个客户组织的 SaaS 部署。
- 想要在业务单元之间强隔离的单一组织。
- Demo 与试用环境,不能让数据泄漏到生产 tenant。
工作原理
平台为每个请求从 JWT 解析出一个 MetaContext。MetaContext 携带 tenantId、userId、userPid、username 与 roleIds;环境、成员与数据权限旁路等扩展状态(environmentId、memberId、各类 bypass 标志)存放在独立的 ThreadLocal 中。所有下游代码——包括 DynamicDataMapper、Kafka producer 与文件存储网关——都从 MetaContext 读取,并在缺失时拒绝工作。
后台任务(Kafka 消费者、定时任务)必须显式构造一个 MetaContext,system user id 用 0L;不这么做会让 ab_data_change_log 的 insert 违反 NOT NULL 约束并回滚事务。
数据隔离
| 层 | 强制方式 |
|---|---|
| 数据库 | 每张动态表都有 tenant_id 列;所有查询按它过滤 |
| Repository | DynamicDataMapper 拒绝缺少 tenantId 参数的查询 |
| 行策略 | 与行级 permission 复合(见 安全加固) |
| 缓存 | Redis key 加前缀 aura:t:<tenantId>: |
| 文件 | MinIO 对象 key 加前缀 t/<tenantId>/... |
模型命名空间
Model 按 tenant 划分命名空间。物理表名遵循 mt_<modelCode> 的模式,跨 tenant 共享,由 tenant_id 列强制隔离。一个 tenant 无法枚举或查询另一个 tenant 声明的 model。system tenant 拥有平台级表,例如审计日志 ab_audit_trail。
文件分区
auraboot-files/
t/tenant_001/
uploads/2026/05/28/<ulid>.pdf
exports/qualified-leads-2026-05.csv
t/tenant_002/
uploads/...一个 tenant 的预签名 URL 由只能解析其前缀下对象的 key 签出。即便 URL 泄漏,也不能读到别的 tenant 的文件。
凭据范围
通过 ConnectorCredentialResolver 按 cr_csp_connector_pid 解析的 connector 凭据是按 tenant 划分范围的。tenant A 调用外部 CRM 的 API key 绝不进入 tenant B 的请求上下文。不同 tenant 使用同一个 cr_csp_connector_pid 值会解析到不同的 secret。
Kafka topic 命名
<env>.<tenant-or-shared>.<domain>.<event>| 模式 | 示例 | 范围 |
|---|---|---|
| 共享 topic,tenant 在 payload 里 | prod.shared.record.updated | 默认;tenant id 是 payload 字段 |
| 每租户独立 topic | prod.t.tenant_001.export.requested | 仅当顺序或吞吐要求按 tenant 分区时使用 |
优先用共享 topic 模式。每租户 topic 会成倍放大 broker 资源开销;只为那些负载值得这份开销的 tenant 保留它。
配置
aura:
tenancy:
default-tenant: "tenant_default"
enforce-context: true # 拒绝任何缺 tenant 的请求
background-system-user-id: 0
storage:
file-prefix: "t/${tenantId}/"
cache:
key-prefix: "aura:t:${tenantId}:"
kafka:
topic-pattern: "${env}.shared.${domain}.${event}"
per-tenant-topics: [] # 显式 opt-in 列表新 tenant 由已登录用户的租户选择 / 注册流程创建:前端调用 TenantSelectionController 的 /process 端点提交一个 TenantSelectionRequest,服务层 TenantApplicationService.createTenantForUser(request, user) 在当前用户身份下落地新 tenant(最终走 TenantService.createTenant)。平台会创建 tenant 行,并把该用户接入新 tenant(建立 member、撒入默认角色)。
验证
- 用 tenant A 的 token 调
/api/<pageKey>/list,tenant B 的数据返回 0 行。 - 限定为 tenant A 的 MinIO 预签名 URL,对 tenant B 的前缀返回 403。
- 后台任务在每个动作上都打印
tenant.id;缺值时 fail-fast 报错。 - 删除某个 tenant 会清理其记录、文件与凭据,其他 tenant 不受影响。
- 任何按 tenant 维度的指标序列都是有界的(每个活跃 tenant 至多一条时间序列),不存在无界 label 基数。