# 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`,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 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 内容猜测权限范围。