docs: design auth risk block table
This commit is contained in:
@@ -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`
|
||||
|
||||
|
||||
@@ -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 范围、唯一约束、手机号/微信身份写入规则与迁移策略。
|
||||
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
|
||||
@@ -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. 边界约束
|
||||
|
||||
|
||||
Reference in New Issue
Block a user