docs: design auth risk block table

This commit is contained in:
2026-04-21 02:17:01 +08:00
parent 84842e9475
commit 90bc0edf9a
5 changed files with 317 additions and 1 deletions

View File

@@ -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 范围、唯一约束、手机号/微信身份写入规则与迁移策略。

View File

@@ -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<String>` | 否 | 手动解除时间;为 `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`

View File

@@ -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`