Files
tiku-backend.net/docs/architecture/security-and-tenancy.md
xiong 3385649a8d
Some checks failed
ci / release-gate (push) Has been cancelled
docs(architecture): sync runtime diagram and guidance
2026-08-04 14:40:25 +08:00

146 lines
9.3 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.

# 认证、授权与租户隔离
本文说明当前请求实际经过的安全边界。新增接口、实体或后台任务时,必须保持这些边界闭合。
## 认证入口
API 按客户端边界提供四组认证接口:
- `/api/platform/auth/**`:平台授权域的 Bearer 客户端入口。
- `/api/tenant/auth/**`:租户员工授权域的 Bearer 客户端入口。
- `/api/student/auth/**`:学生客户端入口;与租户端共享本地账号能力,但必须使用租户授权域且匹配当前租户上下文。
- `/api/tenant/auth/browser/**`:租户同源浏览器入口,把 token 写入 HttpOnly Cookie。
前三组 Bearer 路径共享认证实现,但路由与请求中的 Realm 必须一致;平台路径只接受 Platform Realm租户端和学生端路径不接受 Platform Realm。
当前登录方式:
- 平台账号:账号/密码。
- 租户账号:手机号/密码、手机号/短信验证码。
- 租户可配置微信网页授权和微信小程序授权。
主要流程包括短信发送、密码登录、短信登录、微信登录、refresh、logout、logout-all 和首次登录强制改密。具体请求与响应字段以 Scalar 为准。
密码至少 8 位,并必须同时包含字母和数字。连续 5 次失败触发 15 分钟 Identity lockout。短信验证码由本服务生成和哈希发送 Provider 只负责投递;验证码校验最多允许 5 次尝试。
## JWT 与 Session
- access token 使用 RSA SHA-256 签名,默认有效期 15 分钟。
- refresh token 默认有效期 30 天,服务端只保存哈希。
- JWT 必须包含用户、Session、`jti`、签发时间和 `realm`;租户 realm 还必须包含租户 ID。
- 每次 JWT 认证都会核对数据库 Session、用户/成员状态、安全版本和租户上下文,不把 JWT 声明当作永久授权事实。
- refresh token 轮换并检测重放logout 撤销当前 Sessionlogout-all 撤销用户全部 Session。
- 平台 token 只能在平台 Host 使用;租户 token 必须与 Host 或允许路径上的 tenant code 解析结果一致。上下文冲突返回 401不允许静默切换租户。
Production 必须显式配置 JWT `KeyId`、私钥和验证公钥集合,不能使用 Development 临时密钥。
## 浏览器 Cookie 与 CSRF
Browser Auth 使用 access、refresh 和 CSRF Cookie
- access/refresh Cookie 为 HttpOnly。
- Bearer handler 只会在同源浏览器请求中回退读取 access Cookie显式 `Authorization` header 优先。
- 使用 Cookie 的非安全方法必须通过 `BrowserCsrfMiddleware` 的 Origin/Referer 与 CSRF token 校验。
- 跨源浏览器使用必须同时正确配置 `Cors``BrowserAuth:AllowedOrigins`;允许凭据时不能使用通配 Origin。
非浏览器客户端应使用 Bearer token不应复制浏览器 Cookie 流程。
## Realm、Permission、Feature 与 DataScope
每个受保护操作可能同时经过四层判断:
```text
Realmplatform / tenant
+ BackendPermission操作权限
+ SaaSFeature套餐能力
+ DataScope资源范围
```
- Realm 防止平台身份、租户员工和学生身份跨授权域复用。
- `BackendPermission` 控制 `view/manage/read/write/operate` 等操作。
- `SaaSFeature` 是固定代码目录当前包括后台基础、私有题库、练习、作业、考试、词汇、手册、视频、分数线、站点内容、学生管理、学生商城、CRM、推广分佣和教师 AI。
- 菜单由有效 Permission 与 Feature 共同推导,只用于 UI bootstrap不是 API 授权依据。
- DataScope 支持 `All``Restricted``Self`。无法提供可靠资源 predicate 时返回空查询,不能退化为“当前租户全部数据”。
- 套餐状态、Feature override 和额度使用量来自 PostgreSQLRedis 只用于失效通知和缓存,不能成为授权真相。
Controller 默认受 Fallback Policy 保护,匿名接口必须显式标记 `[AllowAnonymous]``EndpointAuthorizationMetadataConvention` 为非匿名 Controller endpoint 补充 realm、module、permission、Feature 操作、All-only DataScope 和审计元数据,集成测试从运行时 `EndpointDataSource` 验证覆盖。
## Host 与租户上下文
`TenantResolutionMiddleware` 在认证前解析租户:
1. 对平台 Host不默认建立租户上下文。
2. 对非平台 Host按启用的租户域名查找租户未匹配且不属于豁免路径时返回 404。
3. 只有 `TenantCodePathPrefixes` 明确允许的路径,才能在平台 Host 使用 `x-tenant-code``tenantCode` 解析租户。
4. JWT tenant ID 与已解析租户必须一致,否则认证失败。
Development 默认平台 Host 是 `localhost``127.0.0.1`。Production 启动校验要求:
- 至少一个非 loopback 的正式平台 Host
- 非通配 `AllowedHosts`
- 至少一个合法的 `TrustedProxyAddresses`
- 仅信任一跳且来源位于可信代理列表的 `X-Forwarded-For/Host/Proto`
客户端不得通过任意 header、query 或转发头绕过以上路径和可信代理限制。
### 主域名与 Owner 一次性激活
新租户创建时必须提供主域名。域名统一转换为小写 ASCII/IDN Host并使用随机 32 字节 Base64Url TXT 值验证所有权;只有 DNS 与 TLS 都成功后才进入 `Active`。平台领取 Owner 激活链接前还会校验租户、Active 主域名和试用/订阅状态。
Production 默认激活链接为 `https://{primaryHost}/activate/{activationId}#token={token}`Development 可通过专用 localhost 模板指向租户前端端口。Token 位于 fragment不进入 HTTP 请求、服务器访问日志或 RefererPostgreSQL 只保存 SHA-256 哈希。Grant 绑定签发时的 `DomainId`,浏览器激活要求请求 Host、已解析 Tenant、Grant 和 Active 主域名完全一致。并发签发由事务 advisory lock 和部分唯一索引收敛为一个有效 Grant幂等重放只返回 Grant ID 与过期时间,不能恢复明文。
浏览器激活在受审计事务中消费 Grant、设置密码、清除强制改密状态并创建数据库 Session。成功响应仅写入 HttpOnly access/refresh Cookie 与可读 CSRF Cookie不向 JavaScript 返回 Token Pair密码策略失败会整体回滚Grant 不会提前消费。
## 数据库租户隔离
当前 PostgreSQL 连接角色不依赖 RLS。租户隔离由以下机制共同完成
### 查询
`TikuDbContext` 自动为所有包含 `TenantId` 的实体应用 Query Filter。普通请求只有在租户上下文已解析且 ID 匹配时可见System Scope 才能绕过。
模型启动校验会拒绝:
-`TenantId` 但未实现 `ITenantOwned` 的实体;
- 缺少租户 Query Filter 的实体;
- 未包含 `TenantId` 且未显式声明全局唯一的 unique index
- 租户实体之间未使用租户限定 principal key 的外键。
### 写入
`TenantIsolationSaveChangesInterceptor` 检查新增、修改和删除实体的租户所有权,防止普通请求写入其他租户或伪造 `TenantId`。Controller 与 Service 不应接受可任意填写的租户 ID、owner tenant ID、bucket 或 Secret 引用。
### 数据库约束
能用 FK、unique 和 check 表达的规则优先使用 EF 配置。当前集中 PostgreSQL guard 额外保证:
- `TenantQuestionReference` 只能指向平台公共题或当前租户私题,且 source 必须匹配所有者类型。
- `TaxonomyNode` 的父节点只能属于平台主体或当前租户。
- 已发布 SaaS 套餐版本及其 Feature/额度清单不可修改,并校验订阅与订单快照的一致性。
这些 guard 由 Migration helper 统一安装和移除,不允许在多份 Migration 中复制 SQL。
## System Scope 与后台处理
跨租户 Worker、迁移、seed 和平台级后台操作必须通过 `ITenantContextInitializer.InitializeSystem` 或受审计的 `ITenantExecutionScope` 进入 System Scope并提供明确原因。业务代码不得直接关闭 Query Filter。
- Session、成员、租户和套餐状态始终从 PostgreSQL 重新校验。
- 租户、套餐和 Feature 变更在数据库提交后直接失效当前 API 进程与 Redis 中的相关缓存。
- 后台任务在业务事务提交后持久化到 PostgreSQL独立 Worker 使用租约执行;延时和重试由 `RunAfter` 控制。
## Redis 授权缓存模式
`Security:AuthorizationCache:Mode` 支持三种当前实现模式:
- `Disabled`:直接以 PostgreSQL 完成安全状态和权限读取。
- `Shadow`PostgreSQL 决策仍为准,同时读取、回填并比较 Redis 结果。
- `Active`:优先读取 Redis 安全状态与权限快照;缓存缺失或 Redis 不可用时回退 PostgreSQL。
缓存键按环境、realm、租户、用户和 Session 隔离,不保存 JWT、Refresh Token、手机号或邮箱。权限快照键包含持久化授权版本用户、成员、租户、Session 或角色权限变化会推进版本并触发缓存失效。提交后若 Redis 失效失败,事件保留在 PostgreSQL`AuthorizationCacheInvalidationWorker` 重试;旧版本不会覆盖新版本。
Redis 不是授权事实源。Redis 与 PostgreSQL 同时无法完成安全校验时返回 503 `auth_security_unavailable`,不能使用本地旧快照放行。
## 安全配置门禁
Production 还会在启动时验证 Redis、Data Protection 证书、租户 Secret master key、短信 pepper、CORS 和外部服务配置。完整配置入口见[配置与后台任务](../operations.md)。