159 lines
11 KiB
Markdown
159 lines
11 KiB
Markdown
# 认证、授权与租户隔离
|
||
|
||
本文说明当前请求实际经过的安全边界。新增接口、实体或后台任务时,必须保持这些边界闭合。
|
||
|
||
## 认证入口
|
||
|
||
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 撤销当前 Session,logout-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
|
||
Realm(platform / 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 和额度使用量来自 PostgreSQL;Redis 只用于失效通知和缓存,不能成为授权真相。
|
||
|
||
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 请求、服务器访问日志或 Referer;PostgreSQL 只保存 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 变更在数据库提交后通过 FusionCache 失效本节点、Redis L2 和其他节点的 L1。
|
||
- 后台任务在业务事务提交后持久化到 PostgreSQL,独立 Worker 使用租约执行;延时和重试由 `RunAfter` 控制。
|
||
|
||
## FusionCache 业务缓存
|
||
|
||
租户目录、租户 Feature 快照和租户运行时配置共用命名缓存 `TikuBusiness`。FusionCache 管理独立 L1,不与鉴权使用的 `IMemoryCache` 共享;配置 Redis 时复用现有连接作为 L2,并通过 StackExchange.Redis Backplane 传播 `Set`、`Remove` 和过期通知。缓存 key 与 Backplane channel 使用 `tiku:<environment>:business:v2` 前缀,防止环境间串用。
|
||
|
||
| 数据 | L1 | L2 | 失效方式 |
|
||
| --- | --- | --- | --- |
|
||
| 租户目录命中 | 30 秒 | 300 秒 | TTL;域名状态只查询 Active 租户和域名 |
|
||
| 租户目录未命中 | 20 秒 | 20 秒 | 自适应负缓存 TTL |
|
||
| 租户 Feature 快照 | 30 秒 | 60 秒 | Feature、套餐或租户变化后按读写 key 主动失效 |
|
||
| 租户运行时配置 | 120 秒 | 120 秒 | 配置发布、Owner 激活、Feature 或域名生命周期变化后主动失效 |
|
||
|
||
这些缓存不启用 fail-safe、eager refresh、后台分布式写入或分布式锁。Redis 缺失或暂时不可用时,读取回到 PostgreSQL 工厂;正常 TTL 之外不能继续返回旧租户状态、订阅或 Feature 数据。DbMigrator 只注册本地 L1,不依赖 Redis。FusionCache 不接管 ASP.NET Core Output Cache,也不参与下面的安全状态和版本化权限快照。
|
||
|
||
## 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)。
|