130 lines
7.9 KiB
Markdown
130 lines
7.9 KiB
Markdown
# 认证、授权与租户隔离
|
||
|
||
本文说明当前请求实际经过的安全边界。新增接口、实体或后台任务时,必须保持这些边界闭合。
|
||
|
||
## 认证入口
|
||
|
||
API 支持两组认证接口:
|
||
|
||
- `/api/auth/**` 返回 access token 与 refresh token,适合 Bearer 客户端。
|
||
- `/api/browser-auth/**` 把 token 写入 HttpOnly Cookie,适合同源浏览器客户端。
|
||
|
||
当前登录方式:
|
||
|
||
- 平台账号:账号/密码。
|
||
- 租户账号:手机号/密码、手机号/短信验证码。
|
||
- 租户可配置微信网页授权和微信小程序授权。
|
||
|
||
主要流程包括短信发送、密码登录、短信登录、微信登录、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 主域名和试用/订阅状态。
|
||
|
||
激活链接格式固定为 `https://{primaryHost}/activate/{activationId}#token={token}`。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 变更在数据库提交后直接失效当前 API 进程与 Redis 中的相关缓存。
|
||
- 后台任务在业务事务提交后持久化到 PostgreSQL,独立 Worker 使用租约执行;延时和重试由 `RunAfter` 控制。
|
||
|
||
## 安全配置门禁
|
||
|
||
Production 还会在启动时验证 Redis、Data Protection 证书、租户 Secret master key、短信 pepper、CORS 和外部服务配置。完整配置入口见[配置与后台任务](../operations.md)。
|