Files
tiku-backend.net/docs/architecture/authentication-authorization-security.md

450 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# TIKU SaaS 认证、授权与 Host 安全策略
本文档描述 TIKU Backend 当前生效的安全架构是认证、后台授权、租户隔离、Host 解析、Session、MFA 和短信验证码实现的统一约定。新增接口或修改登录流程时应以本文档和自动化测试为准不能只依赖前端菜单、JWT 字符串或历史 `TenantRole` 约定。
## 1. 安全目标与基本原则
系统同时存在两个互相隔离的授权域:
- `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]`
整体边界如下:
```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 约束
```
## 2. 账号安全底座
账号由 ASP.NET Core Identity 管理,`User` 继承 `IdentityUser<Guid>`Identity 与业务实体共用 `TikuDbContext`。系统不使用 ASP.NET 全局 Role 表,租户与平台后台角色由独立 SaaS RBAC 表维护。
当前固定参数:
- 密码最少 10 位。
- Identity PBKDF2 迭代次数为 210,000。
- 连续 5 次密码失败后锁定 15 分钟。
- 用户 Active 状态在每次 Session 校验时检查;强制改密、退出全部设备和其他账号安全事件同时通过 SecurityStamp 使旧 Session 失效。
- TOTP、恢复码、Authenticator Key 使用 Identity 标准能力。
- 微信等外部身份只保留 provider subject、openid、unionid 等映射,不保存 `session_key` 或原始 secret。
Identity 的 Data Protection key 持久化到 PostgreSQL。Development 可以不使用证书;非 Development 环境必须提供包含私钥的 PKCS#12 证书保护 key ring否则 API 启动失败。
## 3. JWT 与数据库 Session
### 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 格式为:
```text
v2.{t|p}.{tenantId|-}.{sessionId}.{64-byte-random-secret}
```
数据库只保存完整 refresh token 的 SHA-256 hash明文只返回客户端一次。
刷新在事务内完成:
```text
旧 refresh token
|
v
读取并验证当前 Session/用户/realm/tenant/SecurityStamp
|
v
原子设置 revoked=rotated + replacedBySessionId
|
v
创建同 family 的子 Session返回新 access/refresh
```
并发刷新只允许一个请求成功。已轮换 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 类型
| 请求入口 | 租户上下文 | 允许的认证域 | 结果 |
| --- | --- | --- | --- |
| 配置的 Platform Host | 默认无租户 | platform部分白名单路径可显式提供 tenantCode 进入 tenant | 继续处理 |
| Active 租户自定义 Host | 固定为该 Host 对应租户 | tenant | 继续处理 |
| 租户 Host + 不同 tenantCode/header | Host 与输入冲突 | 无 | 403 |
| Platform Host + platform realm + tenantCode | 非法混合 | 无 | 400 |
| 非 Platform、未绑定租户的未知 Host | 无 | 无 | 非豁免路径 404 |
| 未知 Host 上的 platform 登录/refresh/logout/MFA challenge | 无 | 无 | 默认先由 Host 解析返回 404即使路径被配置为豁免平台 Host 二次校验仍返回 400 |
默认 Platform Host 是 `localhost``127.0.0.1`,生产必须通过 `Tenancy:Resolution:PlatformHosts` 配置正式平台域名。
### 4.2 租户 Host 流程演示
假设 `school-a.example.com` 已绑定 Tenant A
```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
四者一致:继续授权
```
如果同一请求携带 Tenant B token
```text
Host -------------> Tenant A
token tid --------> Tenant B
X 不一致
结果 -------------> 403 tenant_context_conflict
```
在租户自定义 Host 上,`x-tenant-code`、query `tenantCode` 或 body 中的 tenant ID 都不能切换到另一个租户。
### 4.3 Platform Host 流程演示
平台管理员登录:
```text
POST https://admin.example.com/api/auth/login/password
{
"realm": "platform",
"identifier": "admin@example.com",
"password": "..."
}
Host 在 PlatformHosts ----是----> tenant context 必须为空
tenantCode ----------不得提供
有效平台角色权限 ----必须存在
后台权限 ------------要求 TOTP/强改密流程
```
platform token 只能在 Platform Host 使用:
```text
platform token + admin.example.com -> 允许继续
platform token + school-a.example.com -> 403
platform token + unknown.example.com -> 拒绝
```
### 4.4 在 Platform Host 访问 tenant realm
统一平台 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 内容猜测权限范围。