Files
Genarrative/docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_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

11 KiB
Raw Permalink Blame History

sms_auth_event 表设计

日期:2026-04-21

1. 文档目的

这份文档用于完成 M2 的第六条任务:设计 sms_auth_event

目标是把短信鉴权事件表固定成一张“短信发送与验证码校验的统计源表”,并明确:

  1. 哪些短信相关动作要写入事件
  2. 哪些动作不应该写进这张表
  3. 发送频控、失败次数限制、captcha 触发、风险保护分别依赖什么统计口径
  4. 它与 auth_risk_blockauth_audit_log 的边界

2. 当前基线

当前 Node 后端已经存在 sms_auth_events 表,并有稳定的读写链路:

  1. POST /api/auth/phone/send-code 在短信发送成功后写入一条 send_code 事件
  2. POST /api/auth/phone/login 在验证码校验成功或失败后写入 verify_code 事件
  3. POST /api/auth/wechat/bind-phone 在绑定手机号时复用同一套 verify_code 事件
  4. POST /api/auth/phone/change 在换绑手机号时复用同一套 verify_code 事件
  5. 发送频控、失败次数限制、captcha 触发与风险保护,全部依赖这张表统计

当前 Node sms_auth_events 字段基线:

  1. id
  2. phone_number
  3. scene
  4. action
  5. success
  6. ip
  7. user_agent
  8. created_at

当前已落地的 scene 基线:

  1. login
  2. bind_phone
  3. change_phone

当前已落地的 action 基线:

  1. send_code
  2. verify_code

3. 表职责边界

3.1 sms_auth_event 负责

  1. 记录短信验证码发送成功事件
  2. 记录短信验证码校验成功或失败事件
  3. 作为手机号维度与 IP 维度的短信鉴权统计源
  4. 为发送频控、失败次数限制、captcha 触发、风险保护提供统一统计基础

3.2 它不负责

  1. 保存验证码明文或验证码 hash
  2. 保存阿里云短信 provider 的完整原始响应
  3. 记录当前风险保护是否仍生效
  4. 记录账号安全动作的长期审计历史

3.3 与其他表的边界

  1. sms_auth_event 负责“发生过哪些短信发送/校验事件”
  2. auth_risk_block 负责“经过统计后当前是否处于保护状态”
  3. auth_audit_log 负责“账号维度发生过哪些安全动作”

4. 访问级别

sms_auth_event 固定为 private table

原因:

  1. 原始手机号、原始 IP、原始 UA 都属于敏感安全数据
  2. 这张表主要服务于后端风控与统计,不直接对前端暴露
  3. 前端不应该直接查询或订阅短信验证码操作明细

5. 字段设计

字段名 类型 必填 说明
sms_event_id String 主键,建议继续沿用 smsev_* 前缀。
phone_e164 String 标准化后的 E.164 手机号。
scene String 业务场景,枚举固定为 loginbind_phonechange_phone
action String 动作类型,枚举固定为 send_codeverify_code
success bool 当前动作是否成功。
ip Option<String> 请求来源 IP缺失时为 null
user_agent Option<String> 请求来源 UA缺失时为 null
created_at String 事件发生时间UTC RFC3339。

补充约束:

  1. 当前阶段不存 provider_request_id,因为这张表不是供应商排障日志表。
  2. 当前阶段不存 provider_codeprovider_message,避免把供应商响应日志职责压进统计源表。
  3. 当前阶段不存 user_id,因为验证码发送和校验发生时,场景并不总是已经稳定归属到某个正式账号。

6. 枚举与语义设计

6.1 scene

当前阶段固定只支持:

  1. login
  2. bind_phone
  3. change_phone

解释:

  1. login 对应手机号验证码登录
  2. bind_phone 对应微信待激活账号绑定手机号
  3. change_phone 对应正式账号更换手机号

6.2 action

当前阶段固定只支持:

  1. send_code
  2. verify_code

解释:

  1. send_code 表示验证码已成功发出
  2. verify_code 表示一次验证码校验尝试已经结束

6.3 success

固定语义:

  1. send_code + success = true 表示供应商确认发送成功
  2. verify_code + success = true 表示验证码校验通过
  3. verify_code + success = false 表示验证码校验失败或已失效

当前阶段特别约束:

  1. send_code + success = false 暂不入表
  2. 发送失败继续由应用日志、供应商日志与 tracing 承担排障,不混入当前统计口径

这样做的原因是:

  1. 现有发送频控按 action = send_code 的总数统计
  2. Node 当前只在发送成功后写事件
  3. 若直接把发送失败也写入,会改变当前频控语义

7. 统计口径设计

7.1 发送频控

固定统计来源:

  1. 按手机号统计 action = send_code 的事件数
  2. 按 IP 统计 action = send_code 的事件数

当前 Node 兼容窗口:

  1. 单手机号:过去 1 天
  2. 单 IP过去 1 小时

7.2 验证失败次数限制

固定统计来源:

  1. 按手机号统计 action = verify_code AND success = false
  2. 按 IP 统计 action = verify_code AND success = false

当前 Node 兼容窗口:

  1. 单手机号:过去 1 小时
  2. 单 IP过去 1 小时

7.3 captcha 触发

固定统计来源:

  1. 按手机号统计 verify_code 失败次数
  2. 按 IP 统计 verify_code 失败次数

说明:

  1. captcha challenge 自身不写进 sms_auth_event
  2. captcha_required 审计事件继续写进 auth_audit_log

7.4 风险保护触发

固定统计来源:

  1. 按手机号统计 verify_code 失败次数
  2. 按 IP 统计 verify_code 失败次数

说明:

  1. 风险保护命中后真正的当前态写进 auth_risk_block
  2. sms_auth_event 只提供统计基础,不直接承载保护状态

8. 写入规则

8.1 POST /api/auth/phone/send-code

固定流程:

  1. 先检查当前手机号 / 当前 IP 是否存在活跃 auth_risk_block
  2. 再根据 sms_auth_event 统计发送频控
  3. 再根据 sms_auth_event 统计决定是否需要 captcha
  4. 短信 provider 返回发送成功后,写入一条:
    • scene = 当前请求场景
    • action = send_code
    • success = true

8.2 POST /api/auth/phone/login

固定流程:

  1. 先检查当前手机号 / 当前 IP 是否存在活跃 auth_risk_block
  2. 再根据 sms_auth_event 统计失败次数限制
  3. 校验成功时写入:
    • scene = login
    • action = verify_code
    • success = true
  4. 校验失败时写入:
    • scene = login
    • action = verify_code
    • success = false
  5. 校验失败写入完成后,再按统计结果决定是否触发 auth_risk_block

8.3 POST /api/auth/wechat/bind-phone

固定流程:

  1. 与手机号登录复用同一套校验前检查
  2. 校验成功写入 bind_phone + verify_code + success = true
  3. 校验失败写入 bind_phone + verify_code + success = false
  4. 校验失败后再决定是否触发 auth_risk_block

8.4 POST /api/auth/phone/change

固定流程:

  1. 与手机号登录复用同一套校验前检查
  2. 校验成功写入 change_phone + verify_code + success = true
  3. 校验失败写入 change_phone + verify_code + success = false
  4. 校验失败后再决定是否触发 auth_risk_block

9. 查询索引与统计要求

9.1 必须具备的唯一约束

  1. sms_event_id 主键唯一

9.2 必须具备的查询索引

  1. (phone_e164, action, created_at DESC) 作用:支撑按手机号统计发送次数与失败次数
  2. (ip, action, created_at DESC) 作用:支撑按 IP 统计发送次数与失败次数
  3. (phone_e164, action, success, created_at DESC) 作用:支撑按手机号统计 verify_code 失败窗口
  4. (ip, action, success, created_at DESC) 作用:支撑按 IP 统计 verify_code 失败窗口

说明:

  1. 当前阶段不强求单独按 scene 建主索引,因为已有统计主要按手机号/IP 与动作窗口展开。
  2. scene 继续作为事件上下文字段保留,便于后续如果要细分某一场景的频控,再追加索引。

10. 读取规则

当前阶段 sms_auth_event 不直接对外暴露 DTO。

它只支撑后端内部这几类聚合查询:

  1. count_since_by_phone(phone_e164, action, success?, since)
  2. count_since_by_ip(ip, action, success?, since)

读取约束:

  1. ip = null 时,按 IP 统计固定返回 0
  2. 统计窗口由 Axum 应用层提供,不把“过去 1 小时”“过去 1 天”写死进表层
  3. 表层只提供原子计数,不在表层拼装“是否需要 captcha / 是否需要封禁”的业务判断

11. 与其他鉴权表的协作规则

11.1 与 auth_risk_block

固定规则:

  1. sms_auth_event 先累积失败事实
  2. Axum 根据失败统计决定是否写入或刷新 auth_risk_block
  3. 不能直接从 auth_risk_block 反推历史失败次数

11.2 与 auth_audit_log

固定规则:

  1. sms_auth_event 不承担安全操作审计展示职责
  2. captcha_requiredrisk_block_phonerisk_block_ip 等用户可见安全事件,继续写进 auth_audit_log
  3. 不允许为了省表而把 sms_auth_event 直接拿去充当账号操作记录

12. 与当前 Node sms_auth_events 的映射关系

Node 列 新字段 迁移规则
id sms_event_id 原样迁移。
phone_number phone_e164 重命名迁移,值原样保留。
scene scene 原样迁移。
action action 原样迁移。
success success 原样迁移。
ip ip 原样迁移。
user_agent user_agent 原样迁移。
created_at created_at 原样迁移。

13. reducer / service 落地约束

13.1 module-auth reducer 层

必须至少具备:

  1. append_sms_auth_event

说明:

  1. 这是一张追加型统计源表,不做就地更新。
  2. 后续若要做归档或清理,再单独增加 maintenance reducer 或离线清理任务。

13.2 Axum 应用层

固定负责:

  1. 标准化手机号为 E.164
  2. 组织 sceneactionsuccess
  3. 在正确的时机写入事件
  4. 基于统计结果决定是否触发 captcha 与 auth_risk_block

14. 不允许的设计漂移

后续实现时禁止出现以下情况:

  1. 把验证码明文、验证码 hash 写进 sms_auth_event
  2. 把阿里云 provider 的完整响应 JSON 直接写进 sms_auth_event
  3. 把当前是否被保护的状态写进 sms_auth_event
  4. 发送失败也无条件入表,却继续沿用当前“按 send_code 总量限流”的统计口径
  5. 为了展示账号安全记录,直接把 sms_auth_event 暴露给前端

15. 本任务完成定义

当以下条件满足时,设计 sms_auth_event 视为完成:

  1. 发送与校验事件的写入范围已经固定。
  2. 当前发送频控、失败限制、captcha 触发与风险保护的统计口径已经固定。
  3. 已和 auth_risk_blockauth_audit_log 明确切开职责。
  4. 后续可以直接按这份文档编码 reducer、计数查询与 Axum 应用层判断逻辑。

16. 依据文件

  1. server-node/src/repositories/smsAuthEventRepository.ts
  2. server-node/src/auth/authService.ts
  3. server-node/src/services/smsVerificationService.ts
  4. server-node/src/routes/authRoutes.ts
  5. server-node/src/db/migrations.ts
  6. packages/shared/src/contracts/auth.ts
  7. docs/prd/ACCOUNT_SYSTEM_AND_LOGIN_ENTRY_PRD_2026-04-09.md
  8. docs/prd/MY_TAB_SETTINGS_AND_SECURITY_PRD_2026-04-16.md
  9. docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md
  10. docs/technical/SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md