8.9 KiB
认证、授权与租户隔离
本文说明当前请求实际经过的安全边界。新增接口、实体或后台任务时,必须保持这些边界闭合。
认证入口
API 支持两组认证接口:
/api/tenant/auth/**返回 access token 与 refresh token,适合 Bearer 客户端。/api/tenant/auth/browser/**把 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;显式
Authorizationheader 优先。 - 使用 Cookie 的非安全方法必须通过
BrowserCsrfMiddleware的 Origin/Referer 与 CSRF token 校验。 - 跨源浏览器使用必须同时正确配置
Cors和BrowserAuth:AllowedOrigins;允许凭据时不能使用通配 Origin。
非浏览器客户端应使用 Bearer token,不应复制浏览器 Cookie 流程。
Realm、Permission、Feature 与 DataScope
每个受保护操作可能同时经过四层判断:
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 在认证前解析租户:
- 对平台 Host,不默认建立租户上下文。
- 对非平台 Host,按启用的租户域名查找租户;未匹配且不属于豁免路径时返回 404。
- 只有
TenantCodePathPrefixes明确允许的路径,才能在平台 Host 使用x-tenant-code或tenantCode解析租户。 - 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 变更在数据库提交后直接失效当前 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 和外部服务配置。完整配置入口见配置与后台任务。