docs: simplify migration documentation
This commit is contained in:
@@ -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 时启动失败。
|
||||
|
||||
补强后的每次授权应形成以下闭环:
|
||||
## P0:Disabled / Invited 成员生命周期
|
||||
|
||||
- 外部身份登录不得静默恢复 Disabled membership。
|
||||
- Invited membership 不得被微信登录静默激活。
|
||||
- 首次外部登录是否允许创建学生成员,必须由租户自注册策略控制。
|
||||
- 成员恢复只能由管理员显式操作并写审计。
|
||||
|
||||
验收:
|
||||
|
||||
- Disabled 成员旧 access/refresh 立即失效。
|
||||
- Disabled 成员不能通过微信 Web 或小程序登录恢复。
|
||||
- 关闭自注册时,首次外部登录被拒绝。
|
||||
|
||||
## P1:SaaS 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 改变安全上下文 | 1~2 天 | 生产前必须完成 |
|
||||
| P0 | 成员禁用生命周期 | 禁止外部登录恢复 Disabled 成员 | 0.5~1 天 | 生产前必须完成 |
|
||||
| P1 | SaaS Capability 授权 | 统一套餐、模块、权限、DataScope 和 MFA | 3~5 天 | SaaS 商业化前必须完成 |
|
||||
| P1 | 接口最小权限审计 | 防止过宽 policy 和 DataScope 漏检 | 2~4 天 | 新后台全面接入前完成 |
|
||||
| P1 | System Scope 审计 | 约束并持久化跨租户特权操作 | 2~3 天 | Worker/平台操作上线前完成 |
|
||||
| P2 | Token、隐私与协议规范 | 降低浏览器、PII 和自定义协议风险 | 3~7 天 | 正式客户端规模化前完成 |
|
||||
|
||||
推荐交付顺序:
|
||||
|
||||
```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 manifest:method、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 或后台任务异步执行
|
||||
## P1:System 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. P2:Token、隐私与协议规范
|
||||
|
||||
### 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,不继续扩展私有协议。
|
||||
|
||||
@@ -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 family;logout-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)。
|
||||
|
||||
Reference in New Issue
Block a user