8.6 KiB
8.6 KiB
auth_risk_block 表设计
日期:2026-04-21
1. 文档目的
这份文档用于完成 M2 的第五条任务:设计 auth_risk_block。
目标是把鉴权风控保护表明确为“当前生效态真相表”,并固定:
- 什么叫活跃封禁
- 什么叫解除封禁
- 它与
auth_audit_log的边界 /api/auth/risk-blocks与/api/auth/risk-blocks/:scopeType/lift的读写语义
2. 当前基线
当前 Node 后端已经存在 auth_risk_blocks 表,并具备以下能力:
- 当手机号验证码失败次数达到阈值时,创建或刷新手机号保护
- 当 IP 验证失败次数达到阈值时,创建或刷新网络保护
GET /api/auth/risk-blocks返回当前用户手机号与当前请求 IP 命中的保护POST /api/auth/risk-blocks/:scopeType/lift支持用户主动解除当前手机号或当前网络保护
当前 Node auth_risk_blocks 字段基线:
idscope_typescope_keyreasonexpires_atlifted_atcreated_atupdated_at
当前已落地的 scope 基线:
phoneip
当前已落地的 reason 基线:
sms_verify_failures
3. 表职责边界
3.1 auth_risk_block 负责
- 记录某个手机号或某个 IP 当前是否处于保护状态
- 记录这次保护何时过期
- 记录这次保护是否已被手动解除
- 作为
/api/auth/risk-blocks的唯一事实来源
3.2 它不负责
- 记录所有历史触发次数
- 记录所有历史解除动作
- 生成用户可读标题和说明文案
- 短信频控与失败次数统计
3.3 与其他表的边界
sms_auth_event负责失败次数统计源数据auth_risk_block负责统计之后得到的“当前保护状态”auth_audit_log负责记录“何时触发保护 / 何时解除保护”的历史事实
4. 访问级别
auth_risk_block 固定为 private table。
原因:
- 原始手机号与原始 IP 都属于敏感安全数据
- 前端只应该通过已鉴权接口读取当前命中的保护摘要
- 不能让客户端直接订阅或查询全表
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 |
是 | 最近一次刷新保护或解除保护的时间。 |
补充约束:
- 当前阶段不额外存
user_id,因为 IP 保护天然不是账号主键作用域。 - 当前阶段不额外存
remaining_seconds,读取时按expires_at动态计算。 - 当前阶段不把标题或 detail 文案入库,继续由 Axum 读取时派生。
6. 作用域设计
6.1 phone
固定规则:
scope_key必须是标准化后的E.164手机号- 主要用于手机号验证码登录、绑定手机号、换绑手机号
- 查询时通过当前账号的主手机号命中
6.2 ip
固定规则:
scope_key为请求来源 IP 原文- 主要用于防御同一网络环境的异常尝试
- 查询时通过当前请求上下文中的 IP 命中
7. 活跃态定义
一条 auth_risk_block 只有同时满足以下条件,才视为活跃:
lifted_at = nullexpires_at > now
说明:
- 超时失效不需要额外更新行状态。
- 手动解除必须显式写
lifted_at。
8. 写入规则
8.1 创建或刷新保护
触发点:
- 手机号验证码失败次数达到手机号阈值
- IP 验证失败次数达到 IP 阈值
写入规则:
- 先按
(scope_type, scope_key)查当前活跃保护 - 若已有活跃保护,则更新:
reason_codeexpires_atupdated_at
- 若不存在活跃保护,则新建一条记录
关键约束:
- 风控表是“当前态表”,因此允许刷新同一条活跃记录
- 触发保护的历史事实不在这里重复保留,由
auth_audit_log承担
8.2 手动解除保护
触发点:
POST /api/auth/risk-blocks/phone/liftPOST /api/auth/risk-blocks/ip/lift
写入规则:
- 只更新当前活跃保护
- 写入:
lifted_at = nowupdated_at = now
- 不删除行
8.3 自动超时
触发点:
- 当前时间超过
expires_at
写入规则:
- 不做同步更新
- 查询活跃状态时自然排除
9. 唯一约束与索引
9.1 必须具备的唯一约束
risk_block_id主键唯一
9.2 必须具备的查询索引
(scope_type, scope_key, expires_at DESC)作用:查当前活跃保护(scope_type, scope_key, lifted_at, expires_at DESC)作用:提升当前活跃保护查找效率(expires_at, lifted_at)作用:后续定时清理或后台巡检
说明:
- 当前阶段不强行加“同 scope 只有一条记录”的数据库唯一约束,因为历史已失效/已解除记录允许共存。
- 活跃唯一性由“查询时只取未解除且未过期的最新一条”保证。
10. 对外读取规则
10.1 /api/auth/risk-blocks
固定读取:
- 当前账号主手机号命中的活跃
phone保护 - 当前请求 IP 命中的活跃
ip保护
固定返回:
scopeTypetitledetailexpiresAtremainingSeconds
其中:
title读取时按scope_type派生detail读取时按scope_type + expires_at派生remainingSeconds读取时按expires_at - now计算
10.2 标题派生规则
phone->手机号保护中ip->当前网络保护中
10.3 detail 派生规则
phone->该手机号因异常尝试已被临时保护,请约 N 分钟后再试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 层
必须至少具备:
upsert_auth_risk_blocklift_auth_risk_block
12.2 Axum 应用层
固定负责:
- 根据
sms_auth_event统计结果决定是否触发保护 - 计算新的
expires_at - 读取保护时派生标题、detail 与剩余秒数
- 解除保护成功后同步写
auth_audit_log
13. 与 auth_audit_log 的协作规则
13.1 触发保护时
必须:
- 先写或刷新
auth_risk_block - 再写
auth_audit_log
13.2 解除保护时
必须:
- 先写
lifted_at - 再写
risk_unblock_phone或risk_unblock_ip审计
13.3 禁止的反向依赖
禁止:
- 从
auth_audit_log回推当前是否仍在保护中
14. 不允许的设计漂移
后续实现时禁止出现以下情况:
- 为了保存历史,把每次刷新保护都新建一条新活跃记录而不复用现有活跃记录
- 手动解除时直接删行,导致无法保留解除痕迹
- 把
remainingSeconds这类瞬时值入库 - 把风控表当作审计表使用
- 把
scope_key脱敏后再入库,导致无法稳定命中当前作用域
15. 本任务完成定义
当以下条件满足时,设计 auth_risk_block 视为完成:
- 当前态与历史态边界已经和
auth_audit_log切开。 - scope、活跃态、刷新态、解除态的规则已明确。
/api/auth/risk-blocks与/lift的读取和写入语义已固定。- 后续可以直接按这份文档编码 reducer、view 与 Axum 接口。
16. 依据文件
server-node/src/repositories/authRiskBlockRepository.tsserver-node/src/auth/authService.tsserver-node/src/routes/authRoutes.tsserver-node/src/db/migrations.tspackages/shared/src/contracts/auth.tsdocs/prd/MY_TAB_SETTINGS_AND_SECURITY_PRD_2026-04-16.mddocs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.mddocs/technical/SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md