docs: simplify migration documentation

This commit is contained in:
2026-07-28 16:22:50 +08:00
parent b8d14e8a7e
commit 5732df8886
13 changed files with 484 additions and 2016 deletions

View File

@@ -1,415 +1,85 @@
# TIKU SaaS 认证与授权补强计划
# 认证与授权补强清单
状态:待实施
当前生效规则见 [认证、授权与 Host 安全策略](authentication-authorization-security.md)。本文只记录尚需补强的安全事项,不重复描述已实现体系。
制定日期2026-07-28
## P0可信代理与 Host fail-closed
适用范围:`Tiku.Api``Tiku.Application``Tiku.Infrastructure``Tiku.Worker` 及相关 PostgreSQL 集成测试
- Production 必须配置正式 `PlatformHosts``TrustedProxyAddresses`
- Production 不允许只保留 `localhost` / `127.0.0.1` 作为平台 Host。
- 未受信来源伪造 `X-Forwarded-Host` 不能改变 realm 或 tenant context。
- 受信代理只接受一跳转发,网关必须覆盖客户端伪造的 Forwarded Headers。
- API 公网入口必须只能由受信网关访问。
本文档是在现有《TIKU SaaS 认证、授权与 Host 安全策略》基础上制定的实施计划。现有体系已经具备 ASP.NET Core Policy-based Authorization、数据库 Session、tenant/platform realm、数据库 RBAC、MFA、DataScope、EF Core tenant query filter、写入拦截器和 PostgreSQL 约束。本计划不推翻这些边界而是按风险优先级补齐生产安全、SaaS 套餐授权、最小权限验证、特权审计和客户端认证规范。
验收:
## 1. 目标与原则
- Host A + Tenant B token 返回 403。
- 未知 Host 的非豁免路径返回 404。
- Production 缺少可信代理或正式平台 Host 时启动失败。
补强后的每次授权应形成以下闭环:
## P0Disabled / Invited 成员生命周期
- 外部身份登录不得静默恢复 Disabled membership。
- Invited membership 不得被微信登录静默激活。
- 首次外部登录是否允许创建学生成员,必须由租户自注册策略控制。
- 成员恢复只能由管理员显式操作并写审计。
验收:
- Disabled 成员旧 access/refresh 立即失效。
- Disabled 成员不能通过微信 Web 或小程序登录恢复。
- 关闭自注册时,首次外部登录被拒绝。
## P1SaaS Capability 授权
RBAC 只回答“用户是否有操作权限”Capability 负责“租户是否购买、启用并可使用该能力”。
默认组合:
```text
User Active
+ Session Active
+ Host / Realm / Tenant 一致
+ Tenant Active
+ Membership Active
Tenant Active
+ Subscription 有效
+ 套餐包含目标模块
+ Module / Feature 可用
+ Operation Permission
+ DataScope / Resource Scope
+ 敏感操作 MFA
+ 必要时 MFA
```
实施遵循以下原则
1. 先处理可导致安全边界绕过或权限生命周期失效的问题,再扩展产品能力。
2. 所有安全配置默认 fail-closed不能只依赖部署文档提醒。
3. JWT 继续只表达已认证会话,不承载可直接授权的角色和权限。
4. 前端菜单、feature flag 和页面隐藏永远不能代替 API 授权。
5. 列表、详情、写入、批量、导出和异步任务使用一致的数据范围。
6. 每个阶段独立实现、独立测试、独立提交;未通过本阶段验收不得进入下一阶段。
## 2. 优先级与交付顺序
| 优先级 | 阶段 | 目标 | 预计工作量 | 发布要求 |
| --- | --- | --- | ---: | --- |
| P0 | 可信代理与 Host 边界 | 阻止伪造 Forwarded Host 改变安全上下文 | 12 天 | 生产前必须完成 |
| P0 | 成员禁用生命周期 | 禁止外部登录恢复 Disabled 成员 | 0.51 天 | 生产前必须完成 |
| P1 | SaaS Capability 授权 | 统一套餐、模块、权限、DataScope 和 MFA | 35 天 | SaaS 商业化前必须完成 |
| P1 | 接口最小权限审计 | 防止过宽 policy 和 DataScope 漏检 | 24 天 | 新后台全面接入前完成 |
| P1 | System Scope 审计 | 约束并持久化跨租户特权操作 | 23 天 | Worker/平台操作上线前完成 |
| P2 | Token、隐私与协议规范 | 降低浏览器、PII 和自定义协议风险 | 37 天 | 正式客户端规模化前完成 |
推荐交付顺序:
```text
可信代理
-> Disabled 成员
-> Capability / 套餐授权
-> 接口权限清单与 DataScope
-> System Scope 审计
-> 浏览器 Token、PII 和 OIDC 演进
```
## 3. P0收紧可信代理和 Host 边界
### 3.1 风险
系统把 Host 作为 tenant/platform realm 的安全上下文,但当前 Forwarded Headers 配置会清空框架默认的 `KnownProxies``KnownIPNetworks`。如果生产环境没有提供有效 `TrustedProxyAddresses`,应用不会以 fail-closed 方式拒绝非可信来源提供的 `X-Forwarded-Host`
### 3.2 实施范围
1.`TenantResolutionOptions` 增加生产配置校验:
- Production 必须配置至少一个正式 `PlatformHosts`
- Production 必须配置至少一个合法 `TrustedProxyAddresses`
- Production 不得只保留 `localhost``127.0.0.1` 作为 Platform Host。
- 代理地址无效时应用启动失败。
2. 调整 `ForwardedHeadersOptions`
- 只有存在合法可信代理配置时,才替换框架默认 proxy/network 限制。
- 保持 `ForwardLimit = 1`
- 显式配置 `AllowedHosts`
- 只处理部署所需的 `X-Forwarded-For``X-Forwarded-Host``X-Forwarded-Proto`
3. 收紧 Host 配置:
- 生产 `AllowedHosts` 不允许使用 `*`
- Platform Host、租户公共主域名和允许的网关 Host 必须形成可审计配置。
4. 固化网关契约:
- 网关覆盖客户端提供的 Forwarded Headers。
- API 监听端口不可绕过网关直接暴露公网。
- 网络 ACL 只允许受信网关访问 API。
### 3.3 自动化验收
- 未受信来源伪造 Platform Host不能进入 platform realm。
- 未受信来源伪造其他租户 Host不能改变 tenant context。
- 受信代理转发 Active 租户域名,可以解析正确租户。
- Tenant A Host 携带 Tenant B token返回 403。
- 未知 Host 的非豁免路径返回 404。
- Production 缺少 Platform Host 或可信代理时启动失败。
- 伪造 Forwarded Headers 的测试必须经过完整 HTTP middleware pipeline不能只单测 `TenantResolutionMiddleware`
### 3.4 建议提交
```text
fix(security): fail closed on forwarded host trust
```
## 4. P0修正 Disabled 成员生命周期
### 4.1 固定状态语义
| Membership 状态 | 登录行为 | 状态改变方式 |
| --- | --- | --- |
| Active | 允许继续认证 | 正常业务流程 |
| Invited | 不得静默激活 | 显式接受邀请或管理员确认 |
| Disabled | 所有登录方式拒绝 | 仅管理员显式恢复 |
| 不存在 | 根据租户自注册策略决定 | 创建新 Student 或拒绝 |
### 4.2 实施范围
1. 外部身份登录只能创建从未存在过的 Student membership不能恢复历史 membership。
2. 查询到 Disabled membership 时返回统一租户访问拒绝,不能改回 Active。
3. 查询到 Invited membership 时进入显式邀请接受流程,不能由微信登录静默激活。
4. 建议增加租户级注册策略:
- `AllowStudentSelfRegistration`
- `AllowedSelfRegistrationProviders`
- `RequireRegistrationApproval`
5. 成员恢复必须复用管理员权限、DataScope 和审计,不允许隐藏在登录流程里。
### 4.3 自动化验收
- Disabled Student 不能通过微信 Web 或小程序登录恢复状态。
- Disabled Student 的旧 access token 和 refresh token 继续立即失效。
- Invited 成员不会被外部身份登录静默转为 Active。
- 允许自注册时,首次微信登录可以创建 Student membership。
- 关闭自注册时,首次微信登录被拒绝。
- 管理员显式恢复并写入审计后,成员才能重新登录。
### 4.4 建议提交
```text
fix(auth): preserve disabled tenant membership state
```
## 5. P1建立统一 SaaS Capability 授权层
### 5.1 目标
现有 RBAC 继续负责“用户是否有操作权限”,新增 Capability 层负责“租户当前是否购买、启用并可使用该能力”。两者必须同时成功。
### 5.2 Application 抽象
建议在 Application 层新增只读上下文:
```csharp
public interface ICurrentTenantCapabilityContext
{
Task<CurrentTenantCapabilitySnapshot> GetAsync(
CancellationToken cancellationToken = default);
}
```
Snapshot 至少表达:
- `TenantId`
- `SubscriptionStatus`
- `SubscriptionExpiresAt`
- `EnabledModules`
- `FeatureFlags`
- `UsageLimits`
- `IsReadOnly`
- `DenialReason`
建议增加以下 Authorization Requirement
- `TenantSubscriptionRequirement`
- `TenantModuleRequirement(moduleCode)`
- `TenantUsageRequirement(resourceCode)`
### 5.3 Policy 组合
Controller 不得分别手工检查套餐和权限。现有 permission policy 应由统一注册器或自定义 policy provider 组合:
```text
tenant:content:manage
= CurrentTenantMember
+ ActiveSubscription
+ TenantModule(content)
+ TenantPermission(tenant:content:manage)
+ MFA
```
平台权限和租户权限继续保持 realm 隔离。平台管理员不能因为拥有平台权限而自动读取租户业务数据,跨租户操作必须进入受控 System Scope。
### 5.4 订阅状态语义
实施前固定以下默认规则,产品有不同要求时必须在代码和测试中显式调整:
| 订阅状态 | 历史读取 | 新增业务数据 | 后台配置 |
| --- | --- | --- | --- |
| Trial | 允许 | 允许,受额度限制 | 允许 |
| Active | 允许 | 允许 | 允许 |
| PastDue | 允许 | 默认拒绝或只读 | 仅账单相关 |
| Cancelled | 允许导出和历史查询 | 拒绝 | 仅账单和迁出 |
| Expired | 按保留期只读 | 拒绝 | 仅恢复订阅 |
租户不存在、模块未购买、订阅失效和额度耗尽应使用不同内部错误码,不能全部退化成同一个未找到响应。
### 5.5 自动化验收
验收
- 有 permission 但套餐不含模块,返回 403。
- 套餐包含模块但没有 permission返回 403。
- 套餐和 permission 都有效时允许访问
- PastDue、Cancelled 或 Expired 不能创建新的受限资源
- 修改套餐或模块后,旧 access token 不需要等待过期即可失去能力。
- 前端 feature flag 关闭或开启都不能绕过后端 module requirement。
- 公共题库订阅规则继续通过统一 Capability 或受控领域 policy 执行。
- PastDue / Cancelled / Expired 不能创建新的受限资源
- 修改套餐后,旧 access token 不需要等待过期即可失去能力
### 5.6 建议提交
```text
feat(authz): enforce tenant subscription capabilities
```
## 6. P1接口最小权限与 DataScope 审计
### 6.1 接口授权清单
建立代码化或可由测试读取的 endpoint authorization manifest每个接口至少记录
- HTTP method 与 route
- tenant/platform realm
- 是否允许匿名
- module code
- permission code
- DataScope 策略
- 是否要求 MFA
- 审计 action
示例:
| 路由 | Realm | 模块 | Permission | DataScope |
| --- | --- | --- | --- | --- |
| `POST /tenant/students` | tenant | student | `tenant:student:manage` | Region/Class |
| `PUT /tenant/providers` | tenant | provider | `tenant:provider:manage` | All-only |
| `GET /platform/tenants` | platform | platform-admin | `platform:tenant:manage` | Platform |
| `POST /content/questions` | tenant | content | `tenant:content:manage` | Owner/Region |
### 6.2 架构测试
## P1接口最小权限与 DataScope 审计
- 建立 endpoint authorization manifestmethod、route、realm、module、permission、DataScope、MFA、audit action。
- 后台写接口不得只使用 `[Authorize]`
- 后台管理接口不得只使用 `CurrentTenantMember`
- tenant Controller 不得引用 platform permission反之亦然
- permission 必须属于 endpoint 声明的 module
- All-only 操作必须显式声明,不能隐式放宽。
- `[AllowAnonymous]` 只能出现在审核后的白名单路由。
- tenant/platform 权限不得串用
- `[AllowAnonymous]` 只能出现在白名单路由
- All-only 资源必须显式声明
- 新增 Controller action 未进入 manifest 时测试失败。
- `TenantResourceAccessRequirement` 不能单独代替 operation permission 和 MFA。
### 6.3 DataScope 测试矩阵
验收:
每类资源至少覆盖:
- 列表、详情、创建、更新、删除、批量、导出和 Worker job 使用一致 DataScope。
- 租户 A 管理员不能读取或操作租户 B 数据。
- 列表过滤
- 单条详情
- 创建和目标归属验证
- 更新
- 删除
- 批量操作
- 导出
- Worker 或后台任务异步执行
## P1System Scope 审计
必须证明“列表不可见的资源不能通过已知 ID 更新、删除或导出”。无法可靠映射 owner、region 或 class 的资源继续采用 All-only fail-closed。
- `ITenantExecutionScope` 创建 System Scope 时必须记录 caller、reason、target tenant 和 request/job id。
- 平台操作、Worker、迁移验证和受审计公共题库服务才允许使用 System Scope。
- 跨租户写操作必须落 `AuditLog`
### 6.4 交付方式
验收:
按模块拆分窄提交,例如 student、content、commerce、CRM、platform-admin避免一次性修改所有 Controller 和 Service
- 未声明 reason 的 System Scope 创建失败
- Worker scope 不串租户。
- 高风险平台操作都有审计记录。
## 7. P1收紧 System Scope 并持久化审计
## P2客户端与协议规范
### 7.1 结构化上下文
用结构化请求代替单一 reason 字符串,至少包含:
- `Operation`
- `TargetTenantId`
- `ActorUserId` 或 Worker identity
- `JobId`
- `CorrelationId`
- `Reason`
- `RequestedAt`
### 7.2 持久化审计
System Scope 进入和退出都写数据库:
- `system_scope.entered`
- `system_scope.completed`
- `system_scope.failed`
失败操作也必须保留审计。日志用于可观测性,数据库审计用于业务追溯,二者不能互相替代。
### 7.3 调用边界
- Controller 不得直接注入 `ITenantExecutionScope`
- 普通 tenant service 不得取得全局 System Scope。
- 仅平台管理、Worker、迁移、公共题库和明确审核的基础设施服务可以使用。
- 架构测试维护允许调用方白名单。
- 有明确目标租户的操作必须提供 `TargetTenantId`;只有真正的全局任务可以为空。
### 7.4 自动化验收
- 普通请求 scope 不能切换 tenant。
- System Scope 必须创建新的 DI scope。
- System Scope 仍不能修改已有实体的 TenantId。
- 成功和失败操作都有持久化审计。
- 平台 actor、目标租户、job、reason 和 trace 信息完整。
- 未在白名单中的服务引用 `ITenantExecutionScope` 时架构测试失败。
### 7.5 建议提交
```text
feat(audit): persist privileged tenant scope operations
```
## 8. P2Token、隐私与协议规范
### 8.1 客户端认证策略
按客户端明确契约:
- App、小程序可以继续使用 JSON access/refresh token。
- 浏览器后台:优先评估 BFF 或 `HttpOnly + Secure + SameSite` Cookie。
- 若浏览器继续使用 Bearer token必须明确 refresh token 存储、CSP、XSS 防护和清理策略。
- Cookie 与 Bearer 并存时必须配置清晰的 authentication scheme 选择规则。
### 8.2 Token 最小化
Access token 原则上只保留:
```text
sub sid jti iat iss aud exp scope tid amr
```
手机号和邮箱不是授权必需 claim客户端可以通过 `/api/me` 获取,避免 token 泄漏扩大 PII 暴露。
### 8.3 登录审计隐私
- 手机号和邮箱采用脱敏展示。
- 使用服务端 HMAC 摘要支持稳定关联分析,不直接依赖完整 identifier。
- 定义登录事件、IP、User-Agent 的保留期限和清理任务。
- 限制查询和导出审计日志的权限。
- 审计导出和清理本身也要记录审计。
### 8.4 OIDC 演进条件
短期可以保留当前第一方 JWT/Session 体系。出现以下任一需求时,应采用标准 Identity Provider 或 OpenIddict 等 OIDC/OAuth 方案,而不是继续扩展自定义协议:
- 第三方应用接入
- 企业 SSO
- Authorization Code + PKCE
- 标准 discovery/JWKS
- 多 API audience
- 联邦身份
- service-to-service client credentials
## 9. 测试与发布门槛
完成补强后,生产发布必须满足:
1. `dotnet test TIKU-BACKEND.slnx` 全量通过。
2. PostgreSQL 特有行为由真实 PostgreSQL 集成测试证明,不能使用 EF InMemory 替代。
3. Production 缺少 JWT key、Data Protection 证书、短信 pepper、Platform Host 或可信代理时启动失败。
4. 所有 Controller action 都进入 endpoint authorization manifest。
5. 每个后台模块至少覆盖无权限、无套餐、跨租户、越 DataScope 和无 MFA 的负向测试。
6. Disabled 用户、成员、租户、角色和 permission 撤销能立即使旧 Session 失效。
7. Forwarded Host 伪造测试通过。
8. Refresh token 并发、轮换和重放撤销测试继续通过。
9. System Scope 全部调用点通过架构白名单,并产生持久化审计。
10. 迁移 SQL 已人工检查 tenant-qualified foreign key、唯一约束、check constraint 和新增审计结构。
11. 执行 `dotnet format TIKU-BACKEND.slnx --verify-no-changes --no-restore`
12. 执行 `git diff --check`
## 10. 阶段验收与版本划分
### 安全基线版本
完成:
- P0 可信代理与 Host 边界
- P0 Disabled 成员生命周期
该版本解决已识别的直接安全边界和状态恢复风险。
### SaaS 授权完整版本
完成:
- P1 Capability / 套餐授权
- P1 endpoint manifest 与 DataScope 审计
- P1 System Scope 持久化审计
该版本形成套餐、模块、操作权限、数据范围和特权操作的完整 SaaS 授权闭环。
### 客户端与合规版本
完成:
- P2 浏览器认证策略
- Token claim 最小化
- PII 保留与脱敏
- OIDC 演进决策
该版本用于正式客户端规模化、第三方接入和后续合规治理。
## 11. 明确不在本计划内的事项
- 不引入 PostgreSQL RLS继续使用请求租户上下文、EF Query Filter、写入拦截器、组合约束和真实 PostgreSQL 测试。
- 不把 tenant/platform permission 放入 JWT 作为直接授权事实。
- 不允许前端菜单或 feature flag 成为唯一授权依据。
- 不因为补强认证体系而重新引入 Supabase 运行时依赖。
- 不在没有第三方接入需求时立即重写为完整 OIDC Provider。
- 浏览器 token 存储策略在正式前固定:纯 Bearer、本域 BFF 或 cookie 方案只能选一种主链路。
- Access token 继续短期有效,不把角色和权限写入 JWT。
- 登录审计和错误响应避免泄露手机号、openId、邮箱完整值。
- 出现第三方生态登录、开放 API 或多客户端授权需求时,再评估 OpenIddict / OIDC不继续扩展私有协议。

View File

@@ -1,449 +1,128 @@
# TIKU SaaS 认证、授权与 Host 安全策略
# 认证、授权与 Host 安全策略
本文档描述 TIKU Backend 当前生效安全架构是认证、后台授权、租户隔离、Host 解析、Session、MFA 和短信验证码实现的统一约定。新增接口或修改登录流程时,以本文档和自动化测试为准,不能只依赖前端菜单、JWT 字符串历史 `TenantRole` 约定
本文当前生效安全规范。新增接口或修改登录流程时,以本文档和自动化测试为准前端菜单、JWT 字符串历史角色约定不能代替 API 授权
## 1. 安全目标与基本原则
## 授权域
系统同时存在两个互相隔离的授权域:
- `tenant`:租户业务域,必须绑定 Active 租户和 Active `TenantMembership`
- `platform`:平台运营域,只能从配置的 Platform Host 进入,不绑定租户。
- `tenant`:租户业务域,必须绑定一个 Active 租户和一个 Active `TenantMembership`
- `platform`:平台运营域,不绑定租户,只能从配置的 Platform Host 进入。
核心规则:
核心原则:
1. Host、JWT scope、tenant claim、数据库 Session 和请求租户上下文必须一致
2. JWT 只证明一次已认证会话,不承载可直接授权的角色或权限
3. 后台权限每次从数据库角色绑定解析,菜单只负责 UI 展示,不负责 API 授权
4. 数据权限必须进入 SQL无法可靠映射 owner、region 或 class 的资源采用 `All`-only fail-closed不猜测数据归属
5. 用户、成员、租户、后台角色、后台权限、SecurityStamp 或 Session 任一失效,旧 token 都不能继续扩大访问权。
6. 所有 Controller 默认要求认证,公开接口必须显式标记 `[AllowAnonymous]`
整体边界如下:
- Host、JWT scope、tenant claim、数据库 Session 和请求租户上下文必须一致。
- JWT 只证明已认证会话,不承载可直接授权的角色或权限。
- 后台权限每次从数据库角色绑定解析;菜单只控制 UI 展示
- 数据权限必须进入 SQL无法可靠映射 owner、region 或 class 的资源采用 All-only fail-closed
- 用户、成员、租户、后台角色、权限、SecurityStamp 或 Session 任一失效,旧 token 不能继续取得能力
- Controller 默认要求认证;公开接口必须显式 `[AllowAnonymous]`
```text
浏览器 / App
|
| HTTPS + Host + Bearer/refresh/challenge
v
可信反向代理
|
| 仅 TrustedProxyAddresses 可以提供 Forwarded Headers
v
TenantResolutionMiddleware
|
+--> Platform Host --------> platform realm不得出现 TenantId
|
+--> Active Tenant Host ---> tenant realm锁定 Host 对应 TenantId
|
+--> Unknown Host ---------> 非豁免路径 404
v
JWT + 数据库 AuthSession 校验
v
IAuthorizationHandler + ICurrentAccessContext
v
EF tenant filter + DataScope SQL + PostgreSQL 约束
Client
-> Trusted proxy
-> TenantResolutionMiddleware
-> JWT + AuthSession validation
-> Authorization handler + current access context
-> EF tenant filter + DataScope SQL + PostgreSQL constraints
```
## 2. 账号安全底座
## 账号与 Session
账号由 ASP.NET Core Identity 管理`User` 继承 `IdentityUser<Guid>`Identity 与业务实体共用 `TikuDbContext`。系统不使用 ASP.NET 全局 Role 表,租户与平台后台角色由独立 SaaS RBAC 表维护
当前固定参数:
- 密码最少 10 位。
- Identity PBKDF2 迭代次数为 210,000。
- 账号由 ASP.NET Core Identity 管理。
- 密码最少 10 位PBKDF2 迭代次数 210,000。
- 连续 5 次密码失败后锁定 15 分钟。
- 用户 Active 状态在每次 Session 校验时检查;强制改密、退出全部设备和其他账号安全事件同时通过 SecurityStamp 使旧 Session 失效
- TOTP、恢复码、Authenticator Key 使用 Identity 标准能力
- 微信等外部身份只保留 provider subject、openid、unionid 等映射,不保存 `session_key` 或原始 secret
- TOTP、恢复码和 Authenticator Key 使用 Identity 标准能力
- 微信等外部身份只保存 provider subject、openid、unionid不保存 `session_key` 或原始 secret
- Data Protection key 持久化到 PostgreSQL非 Development 环境必须提供带私钥的 PKCS#12 证书保护 key ring
Identity 的 Data Protection key 持久化到 PostgreSQL。Development 可以不使用证书;非 Development 环境必须提供包含私钥的 PKCS#12 证书保护 key ring否则 API 启动失败。
Access token
## 3. JWT 与数据库 Session
- RSA SHA-256 签名Header 必须包含 `kid`
- 固定 15 分钟。
- 包含 `sub``sid``jti``iat``iss``aud``exp``scope`
- tenant token 必须包含 `tid`platform token 禁止包含 `tid`
- 完成 MFA 的 Session 可包含 `amr=mfa`
- 不包含 role 或 permission claim。
### 3.1 Access token
Access token 使用 RSA SHA-256 非对称签名Header 必须包含可识别的 `kid`。生产配置包含当前私钥;轮换期间把仍需验证的旧公钥放入 `PublicKeys`。未知 `kid`、错误签名、弱于 2048 位的 RSA key、把当前 `kid` 重复放入旧公钥集合等配置都会被拒绝。
Access token 固定 15 分钟,并包含:
| Claim | 含义 |
| --- | --- |
| `sub` | Identity User ID |
| `sid` | 当前 `AuthSession` ID |
| `jti` | 当前 access token 的唯一 ID |
| `iat` | 签发时间 |
| `iss` / `aud` / `exp` | issuer、audience 和过期时间 |
| `scope` | `tenant``platform` |
| `tid` | 仅 tenant token 必须包含platform token 禁止包含 |
| `amr=mfa` | 当前 Session 已完成 MFA 时包含 |
JWT 不包含用于 API 授权的 role 或 permission claim。
### 3.2 AuthSession
`auth_sessions` 是 access/refresh 的服务端事实来源,关键字段包括:
- `realm`、可空 `tenant_id``user_id`
- `token_family_id``parent_session_id``replaced_by_session_id`
- refresh token hash、SecurityStamp、MFA 状态;
- expires/revoked 时间与 revoked reason。
数据库 check constraint 保证 tenant Session 必须有 `tenant_id`platform Session 不得有 `tenant_id`。业务代码只能通过 `IAuthSessionStore` 访问 Session。
每次 Bearer token 验证都必须同时确认:
```text
RSA 签名/kid/iss/aud/exp
|
v
sub + sid + jti + iat + scope/tid 结构正确
|
v
Host realm 与 scope/tid 一致
|
v
AuthSession 存在、未撤销、未过期
|
v
Session.user/tenant/realm/MFA 与 JWT 一致
|
v
User Active + SecurityStamp 一致
|
v
tenant: Tenant Active + Membership Active
platform: 仍有有效平台后台权限
|
v
进入 Authorization Handler
```
Session 校验没有配置旁路;不能通过关闭选项把 JWT 降级为纯无状态 token。
### 3.3 Refresh、logout 与重放
Refresh token 格式为:
Refresh token
```text
v2.{t|p}.{tenantId|-}.{sessionId}.{64-byte-random-secret}
```
数据库只保存完整 refresh token 的 SHA-256 hash,明文只返回客户端一次
- 数据库只保存完整 refresh token 的 SHA-256 hash。
- 刷新在事务内轮换 Session。
- 并发刷新只允许一个成功。
- 已轮换 token 被复用时视为重放,撤销整个 token family 并写审计。
- logout 撤销当前 refresh token familylogout-all 更新 SecurityStamp 并撤销用户全部 Session。
刷新在事务内完成:
## Host 与 tenant 解析
```text
旧 refresh token
|
v
读取并验证当前 Session/用户/realm/tenant/SecurityStamp
|
v
原子设置 revoked=rotated + replacedBySessionId
|
v
创建同 family 的子 Session返回新 access/refresh
```
Host 是认证上下文,不是普通参数。`TenantResolutionMiddleware` 在 Authentication 前执行。
并发刷新只允许一个请求成功。已轮换 token 被再次使用时视为重放,整个 token family 被撤销并写入审计。
- `POST /api/auth/logout`:撤销 refresh token 所属 family。
- `POST /api/auth/logout-all`:更新 SecurityStamp并撤销用户全部 Session。
- 成员禁用、租户暂停、用户禁用、后台权限撤销后,旧 access/refresh 均不能继续取得对应后台能力。
## 4. Host 与 realm 安全策略
Host 不是普通路由参数而是认证上下文的一部分。Host 在 Authentication 之前由 `TenantResolutionMiddleware` 解析。
### 4.1 Host 类型
| 请求入口 | 租户上下文 | 允许的认证域 | 结果 |
| 请求入口 | 租户上下文 | 允许 realm | 默认结果 |
| --- | --- | --- | --- |
| 配置的 Platform Host | 默认无租户 | platform部分白名单路径可显式提供 tenantCode 进入 tenant | 继续处理 |
| Active 租户自定义 Host | 固定为该 Host 对应租户 | tenant | 继续处理 |
| 租户 Host + 不同 tenantCode/header | Host 与输入冲突 | 无 | 403 |
| Platform Host | 无租户 | platform白名单入口可用 tenantCode 引导 tenant 登录 | 继续 |
| Active 租户 Host | Host 绑定租户 | tenant | 继续 |
| 租户 Host + 其他 tenantCode/header | 冲突 | 无 | 403 |
| Platform Host + platform realm + tenantCode | 非法混合 | 无 | 400 |
| 非 Platform、未绑定租户的未知 Host | 无 | 无 | 非豁免路径 404 |
| 未知 Host 上的 platform 登录/refresh/logout/MFA challenge | 无 | 无 | 默认先由 Host 解析返回 404即使路径被配置为豁免平台 Host 二次校验仍返回 400 |
| 未知 Host | 无 | 无 | 非豁免路径 404 |
| Pending/禁用域名 | 无 | 无 | 404 |
默认 Platform Host 是 `localhost``127.0.0.1`,生产必须通过 `Tenancy:Resolution:PlatformHosts` 配置正式平台域名。
规则:
### 4.2 租户 Host 流程演示
- 自定义域名不接受 `tenantCode``host` query 或客户端转发头覆盖。
- tenant JWT 的 `tid` 必须与 Host 解析租户一致。
- platform JWT 不能访问租户 Host。
- 平台 Host 上的租户登录引导才允许受控使用 `tenantCode`
- 只接受可信代理写入的 Forwarded Headers直连客户端伪造无效。
假设 `school-a.example.com` 已绑定 Tenant A
## RBAC、菜单与 DataScope
```text
GET https://school-a.example.com/api/me
Host: school-a.example.com
Authorization: Bearer <tenant A token>
租户后台与平台后台角色分离:
Host ----查询----> Tenant A (Active)
token scope ------> tenant
token tid --------> Tenant A
session tenant ---> Tenant A
- 租户角色、权限、菜单、用户角色绑定都带租户上下文。
- 平台角色不带租户键,不能自动读取租户业务数据。
- 菜单只决定 UI bootstrap 展示,不作为 API 授权依据。
- 后台 API 必须声明明确 permission高风险写操作按策略要求 MFA 和审计。
四者一致:继续授权
```
DataScope
如果同一请求携带 Tenant B token
- `All`:当前租户内该模块全部资源。
- `Restricted`:按 region/class/owner 等资源关系过滤。
- `Self`:只允许当前用户关联资源。
- 无法可靠表达资源关系的模块只允许 `All`,不能退化为仅按 tenant 查询。
```text
Host -------------> Tenant A
token tid --------> Tenant B
X 不一致
结果 -------------> 403 tenant_context_conflict
```
## 短信验证码
在租户自定义 Host 上,`x-tenant-code`、query `tenantCode` 或 body 中的 tenant ID 都不能切换到另一个租户
- 验证码生成、哈希、频控、过期和校验由自有业务服务负责
- `ISmsProvider` 只负责发送。
- 发送失败必须记录失败状态,不能留下可验证验证码。
- 登录、绑定、找回密码等场景使用独立 purpose 和频控键。
### 4.3 Platform Host 流程演示
## 审计与错误
平台管理员登录
必须落审计
```text
POST https://admin.example.com/api/auth/login/password
{
"realm": "platform",
"identifier": "admin@example.com",
"password": "..."
}
- 登录、刷新重放、logout-all、强制改密、MFA 变更;
- 角色、权限、成员状态、租户状态、Provider 配置、支付运营动作;
- System Scope 和跨租户平台操作。
Host 在 PlatformHosts ----是----> tenant context 必须为空
tenantCode ----------不得提供
有效平台角色权限 ----必须存在
后台权限 ------------要求 TOTP/强改密流程
```
错误响应:
platform token 只能在 Platform Host 使用:
- 401未认证或 token/session 无效。
- 403已认证但 realm、tenant、permission、DataScope、MFA 或套餐能力不满足。
- 404未知 Host、不可见资源或需要隐藏存在性的资源。
- 响应不得泄露完整手机号、openId、邮箱、密钥、支付账号或内部 provider payload。
```text
platform token + admin.example.com -> 允许继续
platform token + school-a.example.com -> 403
platform token + unknown.example.com -> 拒绝
```
## 生产配置清单
### 4.4 在 Platform Host 访问 tenant realm
- 正式 `PlatformHosts`
- 可信代理地址和网络 ACL。
- 非通配 `AllowedHosts`
- JWT issuer、audience、当前 `KeyId`、RSA 私钥和旧公钥集合。
- Data Protection 证书。
- CORS 明确 Origin。
- Secret encryption key。
- 短信、对象存储、支付、通知和 AI provider 只通过租户 Provider 配置读取密钥。
统一平台 Host 上的部分公共/认证入口允许使用 `x-tenant-code` 或 query `tenantCode` 解析租户。允许的路径前缀由 `TenantCodePathPrefixes` 控制,默认包括 auth、tenant、catalog、assets、scoreline、referral 和支付通知等入口
示例:
```text
POST http://localhost/api/auth/login/password
x-tenant-code: school-a
{
"realm": "tenant",
"tenantCode": "school-a",
"identifier": "13800000000",
"password": "..."
}
Platform Host + 白名单路径 + tenantCode
|
v
解析 Active Tenant A
|
v
后续 token tid / Session tenant / request tenant 必须都是 Tenant A
```
普通业务路径不能借 `x-tenant-code` 任意切换租户。
### 4.5 Forwarded Host 与可信代理
API 可以读取标准 Forwarded Headers并限制 `ForwardLimit=1``TrustedProxyAddresses` 非空时只信任其中配置的代理;按照 ASP.NET Core Forwarded Headers 的语义KnownProxies/KnownNetworks 同时为空会接受任意转发源,因此生产环境必须配置至少一个可信代理地址,且不能把 API 暴露为可绕过网关的公网入口。生产网关必须:
- 覆盖客户端传入的 `X-Forwarded-Host``X-Forwarded-For``X-Forwarded-Proto`
- 只向 API 转发一个经过验证的外部 Host
- 把网关地址加入 `TrustedProxyAddresses`
- 禁止 API 直接暴露到可绕过网关的公网入口。
没有可信代理配置时,不应假设任意客户端提供的 `X-Forwarded-Host` 会被系统信任。
## 5. 登录状态、强制改密与 MFA
所有登录方式统一返回以下四种状态之一:
- `authenticated`
- `mfa_required`
- `mfa_enrollment_required`
- `password_change_required`
拥有任一 tenant/platform 后台权限的账号必须完成 TOTP。登录不会在 MFA 前签发业务 token只返回 5 分钟、一次性 challenge。
```text
账号密码/短信/微信验证成功
|
+--> ForcePasswordChange ------> password_change_required
|
+--> 有后台权限 + 未配置 TOTP -> mfa_enrollment_required
|
+--> 有后台权限 + 已配置 TOTP -> mfa_required
|
+--> 无后台权限 --------------> authenticated
```
TOTP 接口:
- `POST /api/auth/mfa/totp/setup`
- `POST /api/auth/mfa/totp/confirm`
- `POST /api/auth/mfa/totp/verify`
恢复码仅在首次确认 TOTP 时返回一次;每个恢复码只能兑换一次,重放会失败并记录审计。平台 challenge 的 setup/confirm/verify 也必须继续使用 Platform Host。
## 6. tenant/platform RBAC
授权由 `ICurrentAccessContext` 从数据库解析:
```text
当前 User
+-- tenant realm --> Active Membership
| +-- TenantBackendUserRole
| +-- Active TenantBackendRole
| +-- RolePermission
| +-- tenant:* permission
|
+-- platform realm -> PlatformBackendUserRole
+-- Active PlatformBackendRole
+-- RolePermission
+-- platform:* permission
```
后台授权不读取 JWT role claim、`User.PrimaryRole``TenantMembership.Role``TenantMembership` 只表达租户成员状态和业务身份。Tenant Owner 会绑定不可删除的 `tenant_owner` 系统后台角色;平台超级管理员只由 `platform_super_admin` 系统角色绑定产生。
当前基础权限点:
```text
tenant:dashboard:view platform:dashboard:view
tenant:staff:manage platform:tenant:manage
tenant:role:manage platform:staff:manage
tenant:student:manage platform:role:manage
tenant:content:manage platform:question-bank:manage
tenant:settings:manage platform:audit:view
tenant:provider:manage
tenant:commerce:operate
tenant:crm:manage
tenant:commission:manage
tenant:job:manage
```
主要 Authorization Requirement
- `CurrentTenantMemberRequirement`
- `TenantPermissionRequirement(code)`
- `PlatformPermissionRequirement(code)`
- `MfaRequirement`
- `TenantResourceAccessRequirement`
全局 fallback policy 要求认证;后台权限 policy 同时要求数据库 permission 和 MFA。拒绝访问会写统一审计。
### 菜单不是授权
tenant/platform UI bootstrap 只返回当前数据库有效权限对应的 active menu
```text
数据库有效 permissions ---> 过滤 active menus ---> 前端显示
|
+-------------------------------> API Authorization Handler 再验证
```
隐藏菜单不能代替 API 授权;手工调用 URL 仍会经过 policy。
## 7. DataScope
角色 DataScope 合并规则:
- `All`:允许访问当前租户内该模块全部资源,优先级最高。
- `Restricted(regionIds, classIds)`:多个角色的 region/class 取并集,可选包含 Self。
- `Self`:只允许 owner/当前用户关联资源。
资源列表、详情和写操作必须使用同一范围。越权详情或写入统一按未找到处理,返回 404避免泄露资源是否存在。
```text
多个有效角色
|
+--> 任一 All --------------------> All
|
+--> Restricted A + Restricted B -> region/class 并集
|
+--> 只有 Self -------------------> Self
```
当前学生、班级、现代内容管理,以及具备 owner/region 关系的订单、支付、退款和 DirectContent 资源已在 SQL 中应用范围。支付配置、激活码、积分、优惠券、对账、CRM webhook/config 等缺少可靠 owner/region/class 外键的资源只允许 `All`Restricted/Self 账号会 fail-closed不能退化为仅按 tenant 查询。
## 8. 短信验证码安全
短信发送入口为 `POST /api/auth/sms/send`,仅支持 tenant realm响应 `202` 且不返回验证码。
安全策略:
- 使用 `RandomNumberGenerator.GetInt32` 生成 6 位验证码。
- 使用服务端 pepper 的 HMAC-SHA256 保存验证码摘要。
- 同一验证码最多失败 5 次,第 5 次原子标记 `Blocked`
- 正确验证码只能原子消费一次;并发请求只有一个成功。
- 持久化限制 tenant、phone、IP、device 四个维度。
- HTTP 命名限流再按 phone + IP 分区。
- pepper 至少 32 个字符,缺失时启动校验失败。
默认额度:
| 维度 | 默认值 |
| --- | --- |
| tenant | 100 次/小时 |
| phone | 5 次/小时 |
| IP | 20 次/小时 |
| device | 10 次/小时 |
| 验证失败 | 5 次后 Blocked |
## 9. 审计与错误响应
统一审计覆盖:
- 登录成功、失败、锁定;
- MFA enrollment、验证、恢复码
- Session family 撤销、logout-all、refresh 重放;
- 角色、权限、菜单、用户角色绑定;
- 平台管理员 bootstrap
- 已认证用户的授权拒绝。
API 使用 ProblemDetails。常见结果
| 状态 | 场景 |
| --- | --- |
| 400 | realm/tenantCode/Host 契约错误、无效输入 |
| 401 | 未认证、token/Session 无效 |
| 403 | 已认证但权限不足,或 Host 与 token tenant 冲突 |
| 404 | 未知租户 Host、资源不存在或 DataScope 越权 |
| 429 | 密码、短信、MFA 或全局限流 |
## 10. 生产配置清单
上线前至少确认:
1. `Tenancy:Resolution:PlatformHosts` 只包含正式平台域名。
2. `Tenancy:Resolution:TrustedProxyAddresses` 只包含实际网关地址。
3. JWT `Issuer``Audience`、当前 `KeyId`、RSA 私钥和旧公钥集合已配置。
4. Access token 仍固定为 15 分钟,不能关闭数据库 Session 校验。
5. `TIKU_SMS_CODE_PEPPER` 使用独立高熵值,不使用开发默认值。
6. `TIKU_DATA_PROTECTION_CERTIFICATE_PATH` 指向包含私钥的 PKCS#12 文件,并配置密码。
7. CORS 只允许明确 Origin浏览器 cookie/BFF 不在当前 token JSON 契约内。
8. API/Worker 不自动迁移数据库;部署流程显式运行 `Tiku.DbMigrator`
9. 首次部署使用一次性 `--bootstrap-platform-admin`,完成强制改密和 TOTP 后销毁临时密码。
10. 网关阻断未知 Host并禁止绕过可信代理直连 API。
## 11. 新接口安全检查
新增或修改接口时必须回答:
- 它属于 tenant 还是 platform realm
- 是否显式 `[AllowAnonymous]`;若不是,使用哪个 permission policy
- Host、tenant context、JWT `tid` 和 Session 是否能形成一致闭环?
- 是否需要 MFA后台 permission policy 默认需要。
- 资源如何映射 owner、region、class列表和写入是否使用相同 SQL 范围?
- 越权是否返回 404
- 是否写审计?
- 是否需要 account+IP、phone+IP 或持久化多维限流?
- 是否错误地读取 JWT role、PrimaryRole、TenantRole 或前端菜单做授权?
如果资源没有可靠的数据范围关联,先限制为 `All`,再通过明确的 schema 变更补足 owner/region/class 外键;禁止用字符串 ID 或 JSON 内容猜测权限范围。
待补强事项见 [认证与授权待补强清单](authentication-authorization-hardening-plan.md)