Files
Genarrative/docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

8.6 KiB
Raw Blame History

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 保护作用域,枚举固定为 phoneip
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_phonerisk_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