多租户不是“每张表多一个 TenantId”。它是一条从请求证据到数据访问的连续信任链:边界选择有效租户,状态守卫判断是否可服务,上下文把同一个快照传播给 ORM、缓存和审计,后台作业和运维工具则必须显式建立可信作用域。
1. 能力分布在框架、Host 和 Identity
| 源码区域 | 主要类型 | 责任 |
|---|---|---|
| Domain | TenantId、TenancyDefaults、ITenantEntity、TenantAggregateRoot | 稳定标识、默认值和租户模型契约 |
| Application | TenantResolver、八个 ResolutionStep、ITenantGuard | 传输中立的解析和可服务状态协议 |
| Application Abstractions | CurrentTenant、ICurrentTenant、ICurrentTenantAccessor | 不可变快照、只读消费和 AsyncLocal 作用域 |
| API Host | 两个租户 Middleware、HttpContextCurrentTenant | HTTP 输入映射、作用域安装和会话校验 |
| Infrastructure | 持久化执行上下文、EF / SqlSugar QueryFilter | 把有效租户落实到查询和审计 |
| Identity | PlatformTenant、TenantStatusGuard、Host 映射、模拟 Grant/Token 服务 | 租户状态、域名事实和正式运维授权 |
Multitenancy 是跨层基础能力,不是一个独立 src/Platform/Multitenancy 项目。只阅读模块页或只搜索 Platform 会漏掉真正的安全实现。
2. HTTP 主路径
租户解析位于认证之后,因此能使用可信 CurrentUser;租户模拟会话校验位于解析之后,因此读取 ICurrentTenant;授权与业务 Endpoint 位于它们之后。
3. 八步解析链
DI 注册顺序就是优先级:
SystemJobTenantResolutionStep:显式系统作业字段或已有 Ambient Scope;RootOperatorOverrideTenantStep:Host 操作员覆盖或开发调试覆盖;ApplicationCallerTenantStep:已认证 Application 的 TenantId;UserClaimTenantStep:已认证 User 的 TenantId;HostSubdomainTenantStep:完整 Host 到平台租户映射;HeaderTenantStep:X-Tenant只在等于已认证租户时确认;PathTenantStep:路径首段只在等于已认证租户时确认;SingleTenantDefaultTenantStep:固定回退1000001。
Resolver 取第一个满足 TenancyDefaults.IsValid 的字符串。当前所谓“有效”仅表示非空且不等于 "0";TenantId.Of 也不会验证雪花格式、租户存在性或状态。这些由后续 Guard 承担。
详见 租户解析链。
4. 只允许一个租户事实轨道
ICurrentTenantAccessor 使用 AsyncLocal<CurrentTenant>,BeginScope 可嵌套并在 Dispose 时恢复外层。ICurrentTenant 是业务只读入口;旧 ITenantContext 只是同一 Accessor 的兼容投影,不保存第二份字段。
var tenantId = await ownership.ResolveTenantAsync(message.AggregateId, cancellationToken);if (!TenancyDefaults.IsValid(tenantId)){ throw new InvalidOperationException("The message has no trusted tenant owner.");}
// BeginScope 同步推入 CurrentTenant 和持久化执行上下文;Dispose 恢复外层。using var tenantScope = tenantAccessor.BeginScope(new CurrentTenant(tenantId));
// 从这一行开始,Repository、缓存键和审计都读取同一个有效租户。await handler.ProcessAsync(message, cancellationToken);BeginScope 不做 I/O、授权或状态验证。调用方必须先从队列父记录、Grant 或其他可信 owner 解析租户。
5. 数据隔离的两层保证
Tenant 模型实现 ITenantEntity。生成式持久化元数据为 EF Core 组合 Tenant 与软删除全局过滤器;SqlSugar 在 Scope 配置中注册 ITenantEntity Filter。显式忽略软删除时,两个 Repository 仍会重新加回租户谓词。
| Provider | 默认过滤 | 显式包含软删除时 |
| -------- | ------------------------------------------------------- | ------------------------------ | ------------------------------ | --------------------------------------------- |
| EF Core | IsTenantFilterBypass | | TenantId == TenantFilterValue | IgnoreQueryFilters() 后手工恢复 Tenant 谓词 |
| SqlSugar | ITenantEntity.TenantId == resolvedTenant + SoftDelete | 清过滤器后手工恢复 Tenant 谓词 |
业务查询仍应显式表达 owner、Office、部门和 Authorization DataScope;全局 Tenant Filter 只解决租户隔离。
6. Host 覆盖与正式模拟不是同一能力
X-Operate-As-Tenant 只对已认证 CallerType.Host 生效;X-Tenant-Debug 只在 Development 生效。它们当前都写入普通 CurrentTenant,并跳过 TenantStatusGuard。
正式租户模拟模型则要求 TenantImpersonationGrant、最长两小时 Token、操作人租户、GrantId 和独立到期时间,TenantImpersonationTokenMiddleware 会检查过期与主动撤销。
更重要的是,EF Core 与 SqlSugar 当前都让所有已认证 Host 调用者绕过 Tenant Filter。即使 X-Operate-As-Tenant 把 EffectiveTenantId 改为目标租户,Host bypass 仍为真,持久化查询并不会自动收窄到目标租户。这是 GA 前必须修复并做双 ORM 集成测试的安全边界。
详见 运维租户切换与模拟。
7. 状态守卫
生产 TenantStatusGuard 从 ITenantStore 读取租户,只允许 Active 与 GracePeriod。未知、Suspended、Expired、PendingProvisioning 和 Deactivated 均拒绝;默认内存 Store 仅对未注册的 1000001 保留单租户兼容放行。CallerType.System 直接绕过 Guard。
HTTP Middleware 还会对 RootOverride 和 Debug 路径跳过 Guard。因此“所有解析结果都经过状态校验”不是当前事实。
8. 后台任务、缓存和审计
- Job/CAP Consumer 必须从消息 owner 或父记录解析租户,再开
BeginScope; WorkflowTimerJobExecutionScope会从父流程实例解析租户,并专门替换 SqlSugar 的 Tenant Filter;- 缓存键、文件 Object Key、搜索、报表和审计都要包含 EffectiveTenantId;
- 正式模拟时数据范围使用 EffectiveTenantId,审计还应保留 ActorTenantId、ImpersonatorUserId 与 GrantId;
- 系统队列可以跨租户扫描,但每一条业务处理必须回到单租户作用域。
详见 后台任务与租户化资源。
9. 章节地图
| 目标 | 页面 |
|---|---|
| 理解优先级、Host 映射、Header/Path 防伪与默认租户 | 租户解析链 |
| 使用 CurrentTenant 并核验双 ORM QueryFilter | 上下文与持久化 |
| 设计 Host 运维、Grant、Token 和审计边界 | 租户切换与模拟 |
| 编写 Job、Consumer、缓存、文件和搜索适配 | 后台任务与租户化资源 |
| 建立隔离测试、排障和故障演练 | 测试与生产运维 |
10. 源码审查入口
# 同时审查解析、Host 中间件、CurrentTenant 和两个 ORM,不能只看 Application。rg -n "TenantResolver|TenantResolutionMiddleware|CurrentTenant|TenantFilter" \ src/Framework src/Hosts/BitzOrcas.Api src/Platform/Identity -g '*.cs'
# 当前预期能看到 Host bypass 只依赖 CallerType.Host;修复后必须重审本手册。rg -n "IsTenantFilterBypass|isHostBypass|CallerType.Host" \ src/Framework/BitzOrcas.Infrastructure.EfCore src/Framework/BitzOrcas.Infrastructure.SqlSugar -g '*.cs'
# 搜索正式模拟会话的 HTTP 闭环;当前没有签发 Endpoint 或 Claim 还原实现。rg -n "TenantImpersonationTokenDescriptor|TenantImpersonationGrantId" \ src/Hosts src/Platform/Identity -g '*.cs'11. GA 最低证据
- 所有租户解析来源都有明确可信级别和冲突语义;
- Host operate-as 后两个 ORM 都只返回目标租户,而不是全局绕过;
- Override、Debug 和正式模拟目标均检查租户存在与可服务状态;
- 正式模拟签发、Claim、CurrentTenant 还原、到期、撤销和审计完整闭环;
- Job、Consumer、缓存、文件、搜索与报表均有 A/B 租户隔离测试;
IgnoreQueryFilters、QueryFilter.Clear与系统级跨租户操作均被架构或契约测试审查。