From 90bc0edf9a8aec2b2c184ebdc7adf167c87508bc Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Apr 2026 02:17:01 +0800 Subject: [PATCH] docs: design auth risk block table --- .../01_M0_M2_FOUNDATION_AND_AUTH.md | 3 +- docs/technical/README.md | 1 + ...AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md | 309 ++++++++++++++++++ ...M_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md | 4 + server-rs/packages/module-auth/README.md | 1 + 5 files changed, 317 insertions(+), 1 deletion(-) create mode 100644 docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md diff --git a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md index 289e9a00..4d7e982c 100644 --- a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md +++ b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md @@ -148,7 +148,8 @@ 交付物:[../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md) - [x] 设计 `auth_audit_log` 交付物:[../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md) -- [ ] 设计 `auth_risk_block` +- [x] 设计 `auth_risk_block` + 交付物:[../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md) - [ ] 设计 `sms_auth_event` - [ ] 设计 `wechat_auth_state` diff --git a/docs/technical/README.md b/docs/technical/README.md index 01fb85bc..3304fe17 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -4,6 +4,7 @@ ## 文档列表 +- [SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md):`M2` 第五张风控状态表 `auth_risk_block` 的作用域、活跃态、刷新/解除规则与读取派生约束。 - [SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md):`M2` 第四张鉴权审计表 `auth_audit_log` 的事件范围、追加写规则、索引与对外 DTO 派生约束。 - [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md):`M2` 第三张会话表 `refresh_session` 的 cookie/hash 边界、轮换与吊销语义、索引与迁移规则。 - [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md):`M2` 第二张身份表 `auth_identity` 的 provider 范围、唯一约束、手机号/微信身份写入规则与迁移策略。 diff --git a/docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md b/docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md new file mode 100644 index 00000000..c6c0ce3f --- /dev/null +++ b/docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md @@ -0,0 +1,309 @@ +# `auth_risk_block` 表设计 + +日期:`2026-04-21` + +## 1. 文档目的 + +这份文档用于完成 `M2` 的第五条任务:`设计 auth_risk_block`。 + +目标是把鉴权风控保护表明确为“当前生效态真相表”,并固定: + +1. 什么叫活跃封禁 +2. 什么叫解除封禁 +3. 它与 `auth_audit_log` 的边界 +4. `/api/auth/risk-blocks` 与 `/api/auth/risk-blocks/:scopeType/lift` 的读写语义 + +## 2. 当前基线 + +当前 Node 后端已经存在 `auth_risk_blocks` 表,并具备以下能力: + +1. 当手机号验证码失败次数达到阈值时,创建或刷新手机号保护 +2. 当 IP 验证失败次数达到阈值时,创建或刷新网络保护 +3. `GET /api/auth/risk-blocks` 返回当前用户手机号与当前请求 IP 命中的保护 +4. `POST /api/auth/risk-blocks/:scopeType/lift` 支持用户主动解除当前手机号或当前网络保护 + +当前 Node `auth_risk_blocks` 字段基线: + +1. `id` +2. `scope_type` +3. `scope_key` +4. `reason` +5. `expires_at` +6. `lifted_at` +7. `created_at` +8. `updated_at` + +当前已落地的 scope 基线: + +1. `phone` +2. `ip` + +当前已落地的 reason 基线: + +1. `sms_verify_failures` + +## 3. 表职责边界 + +### 3.1 `auth_risk_block` 负责 + +1. 记录某个手机号或某个 IP 当前是否处于保护状态 +2. 记录这次保护何时过期 +3. 记录这次保护是否已被手动解除 +4. 作为 `/api/auth/risk-blocks` 的唯一事实来源 + +### 3.2 它不负责 + +1. 记录所有历史触发次数 +2. 记录所有历史解除动作 +3. 生成用户可读标题和说明文案 +4. 短信频控与失败次数统计 + +### 3.3 与其他表的边界 + +1. `sms_auth_event` 负责失败次数统计源数据 +2. `auth_risk_block` 负责统计之后得到的“当前保护状态” +3. `auth_audit_log` 负责记录“何时触发保护 / 何时解除保护”的历史事实 + +## 4. 访问级别 + +`auth_risk_block` 固定为 `private table`。 + +原因: + +1. 原始手机号与原始 IP 都属于敏感安全数据 +2. 前端只应该通过已鉴权接口读取当前命中的保护摘要 +3. 不能让客户端直接订阅或查询全表 + +## 5. 字段设计 + +| 字段名 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `risk_block_id` | `String` | 是 | 主键,建议继续沿用 `risk_*` 前缀。 | +| `scope_type` | `String` | 是 | 保护作用域,枚举固定为 `phone`、`ip`。 | +| `scope_key` | `String` | 是 | 作用域主体键;`phone` 时为 `E.164` 手机号,`ip` 时为原始 IP。 | +| `reason_code` | `String` | 是 | 触发原因码,当前固定为 `sms_verify_failures`。 | +| `expires_at` | `String` | 是 | 当前保护的失效时间,UTC RFC3339。 | +| `lifted_at` | `Option` | 否 | 手动解除时间;为 `null` 表示未手动解除。 | +| `created_at` | `String` | 是 | 首次创建该保护记录的时间。 | +| `updated_at` | `String` | 是 | 最近一次刷新保护或解除保护的时间。 | + +补充约束: + +1. 当前阶段不额外存 `user_id`,因为 IP 保护天然不是账号主键作用域。 +2. 当前阶段不额外存 `remaining_seconds`,读取时按 `expires_at` 动态计算。 +3. 当前阶段不把标题或 detail 文案入库,继续由 Axum 读取时派生。 + +## 6. 作用域设计 + +### 6.1 `phone` + +固定规则: + +1. `scope_key` 必须是标准化后的 `E.164` 手机号 +2. 主要用于手机号验证码登录、绑定手机号、换绑手机号 +3. 查询时通过当前账号的主手机号命中 + +### 6.2 `ip` + +固定规则: + +1. `scope_key` 为请求来源 IP 原文 +2. 主要用于防御同一网络环境的异常尝试 +3. 查询时通过当前请求上下文中的 IP 命中 + +## 7. 活跃态定义 + +一条 `auth_risk_block` 只有同时满足以下条件,才视为活跃: + +1. `lifted_at = null` +2. `expires_at > now` + +说明: + +1. 超时失效不需要额外更新行状态。 +2. 手动解除必须显式写 `lifted_at`。 + +## 8. 写入规则 + +### 8.1 创建或刷新保护 + +触发点: + +1. 手机号验证码失败次数达到手机号阈值 +2. IP 验证失败次数达到 IP 阈值 + +写入规则: + +1. 先按 `(scope_type, scope_key)` 查当前活跃保护 +2. 若已有活跃保护,则更新: + - `reason_code` + - `expires_at` + - `updated_at` +3. 若不存在活跃保护,则新建一条记录 + +关键约束: + +1. 风控表是“当前态表”,因此允许刷新同一条活跃记录 +2. 触发保护的历史事实不在这里重复保留,由 `auth_audit_log` 承担 + +### 8.2 手动解除保护 + +触发点: + +1. `POST /api/auth/risk-blocks/phone/lift` +2. `POST /api/auth/risk-blocks/ip/lift` + +写入规则: + +1. 只更新当前活跃保护 +2. 写入: + - `lifted_at = now` + - `updated_at = now` +3. 不删除行 + +### 8.3 自动超时 + +触发点: + +1. 当前时间超过 `expires_at` + +写入规则: + +1. 不做同步更新 +2. 查询活跃状态时自然排除 + +## 9. 唯一约束与索引 + +### 9.1 必须具备的唯一约束 + +1. `risk_block_id` 主键唯一 + +### 9.2 必须具备的查询索引 + +1. `(scope_type, scope_key, expires_at DESC)` + 作用:查当前活跃保护 +2. `(scope_type, scope_key, lifted_at, expires_at DESC)` + 作用:提升当前活跃保护查找效率 +3. `(expires_at, lifted_at)` + 作用:后续定时清理或后台巡检 + +说明: + +1. 当前阶段不强行加“同 scope 只有一条记录”的数据库唯一约束,因为历史已失效/已解除记录允许共存。 +2. 活跃唯一性由“查询时只取未解除且未过期的最新一条”保证。 + +## 10. 对外读取规则 + +### 10.1 `/api/auth/risk-blocks` + +固定读取: + +1. 当前账号主手机号命中的活跃 `phone` 保护 +2. 当前请求 IP 命中的活跃 `ip` 保护 + +固定返回: + +1. `scopeType` +2. `title` +3. `detail` +4. `expiresAt` +5. `remainingSeconds` + +其中: + +1. `title` 读取时按 `scope_type` 派生 +2. `detail` 读取时按 `scope_type + expires_at` 派生 +3. `remainingSeconds` 读取时按 `expires_at - now` 计算 + +### 10.2 标题派生规则 + +1. `phone` -> `手机号保护中` +2. `ip` -> `当前网络保护中` + +### 10.3 detail 派生规则 + +1. `phone` -> `该手机号因异常尝试已被临时保护,请约 N 分钟后再试` +2. `ip` -> `当前网络因异常尝试已被临时保护,请约 N 分钟后再试` + +## 11. 与当前 Node `auth_risk_blocks` 的映射关系 + +| Node 列 | 新字段 | 迁移规则 | +| --- | --- | --- | +| `id` | `risk_block_id` | 原样迁移。 | +| `scope_type` | `scope_type` | 原样迁移。 | +| `scope_key` | `scope_key` | 原样迁移。 | +| `reason` | `reason_code` | 重命名迁移,值当前原样保留。 | +| `expires_at` | `expires_at` | 原样迁移。 | +| `lifted_at` | `lifted_at` | 原样迁移。 | +| `created_at` | `created_at` | 原样迁移。 | +| `updated_at` | `updated_at` | 原样迁移。 | + +## 12. reducer / service 落地约束 + +### 12.1 `module-auth` reducer 层 + +必须至少具备: + +1. `upsert_auth_risk_block` +2. `lift_auth_risk_block` + +### 12.2 Axum 应用层 + +固定负责: + +1. 根据 `sms_auth_event` 统计结果决定是否触发保护 +2. 计算新的 `expires_at` +3. 读取保护时派生标题、detail 与剩余秒数 +4. 解除保护成功后同步写 `auth_audit_log` + +## 13. 与 `auth_audit_log` 的协作规则 + +### 13.1 触发保护时 + +必须: + +1. 先写或刷新 `auth_risk_block` +2. 再写 `auth_audit_log` + +### 13.2 解除保护时 + +必须: + +1. 先写 `lifted_at` +2. 再写 `risk_unblock_phone` 或 `risk_unblock_ip` 审计 + +### 13.3 禁止的反向依赖 + +禁止: + +1. 从 `auth_audit_log` 回推当前是否仍在保护中 + +## 14. 不允许的设计漂移 + +后续实现时禁止出现以下情况: + +1. 为了保存历史,把每次刷新保护都新建一条新活跃记录而不复用现有活跃记录 +2. 手动解除时直接删行,导致无法保留解除痕迹 +3. 把 `remainingSeconds` 这类瞬时值入库 +4. 把风控表当作审计表使用 +5. 把 `scope_key` 脱敏后再入库,导致无法稳定命中当前作用域 + +## 15. 本任务完成定义 + +当以下条件满足时,`设计 auth_risk_block` 视为完成: + +1. 当前态与历史态边界已经和 `auth_audit_log` 切开。 +2. scope、活跃态、刷新态、解除态的规则已明确。 +3. `/api/auth/risk-blocks` 与 `/lift` 的读取和写入语义已固定。 +4. 后续可以直接按这份文档编码 reducer、view 与 Axum 接口。 + +## 16. 依据文件 + +1. `server-node/src/repositories/authRiskBlockRepository.ts` +2. `server-node/src/auth/authService.ts` +3. `server-node/src/routes/authRoutes.ts` +4. `server-node/src/db/migrations.ts` +5. `packages/shared/src/contracts/auth.ts` +6. `docs/prd/MY_TAB_SETTINGS_AND_SECURITY_PRD_2026-04-16.md` +7. `docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md` +8. `docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md` diff --git a/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md b/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md index d9c1316b..d680eca6 100644 --- a/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md +++ b/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md @@ -322,6 +322,10 @@ server-rs/ - [SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md) +`auth_risk_block` 的作用域、活跃态与解除规则,见: + +- [SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md) + ### B. 运行时主状态表 - `runtime_snapshot` diff --git a/server-rs/packages/module-auth/README.md b/server-rs/packages/module-auth/README.md index 195b4414..4acc07b5 100644 --- a/server-rs/packages/module-auth/README.md +++ b/server-rs/packages/module-auth/README.md @@ -28,6 +28,7 @@ 2. [../../../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md) 3. [../../../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md) 4. [../../../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md) +5. [../../../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md) ## 3. 边界约束