docs: add authentication hardening plan

This commit is contained in:
2026-07-28 15:59:56 +08:00
parent 28d1a7ac52
commit b8d14e8a7e

View File

@@ -0,0 +1,415 @@
# TIKU SaaS 认证与授权补强计划
状态:待实施
制定日期2026-07-28
适用范围:`Tiku.Api``Tiku.Application``Tiku.Infrastructure``Tiku.Worker` 及相关 PostgreSQL 集成测试。
本文档是在现有《TIKU SaaS 认证、授权与 Host 安全策略》基础上制定的实施计划。现有体系已经具备 ASP.NET Core Policy-based Authorization、数据库 Session、tenant/platform realm、数据库 RBAC、MFA、DataScope、EF Core tenant query filter、写入拦截器和 PostgreSQL 约束。本计划不推翻这些边界而是按风险优先级补齐生产安全、SaaS 套餐授权、最小权限验证、特权审计和客户端认证规范。
## 1. 目标与原则
补强后的每次授权应形成以下闭环:
```text
User Active
+ Session Active
+ Host / Realm / Tenant 一致
+ Tenant Active
+ Membership Active
+ Subscription 有效
+ 套餐包含目标模块
+ Operation Permission
+ DataScope / Resource Scope
+ 敏感操作 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 执行。
### 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 架构测试
- 后台写接口不得只使用 `[Authorize]`
- 后台管理接口不得只使用 `CurrentTenantMember`
- tenant Controller 不得引用 platform permission反之亦然。
- permission 必须属于 endpoint 声明的 module。
- All-only 操作必须显式声明,不能隐式放宽。
- `[AllowAnonymous]` 只能出现在审核后的白名单路由。
- 新增 Controller action 未进入 manifest 时测试失败。
- `TenantResourceAccessRequirement` 不能单独代替 operation permission 和 MFA。
### 6.3 DataScope 测试矩阵
每类资源至少覆盖:
- 列表过滤
- 单条详情
- 创建和目标归属验证
- 更新
- 删除
- 批量操作
- 导出
- Worker 或后台任务异步执行
必须证明“列表不可见的资源不能通过已知 ID 更新、删除或导出”。无法可靠映射 owner、region 或 class 的资源继续采用 All-only fail-closed。
### 6.4 交付方式
按模块拆分窄提交,例如 student、content、commerce、CRM、platform-admin避免一次性修改所有 Controller 和 Service。
## 7. P1收紧 System Scope 并持久化审计
### 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。