From 5a60ab397298078131b402e22f39ef617916fdaf Mon Sep 17 00:00:00 2001 From: kdletters Date: Tue, 21 Apr 2026 10:08:11 +0800 Subject: [PATCH] docs: design sms auth event table --- .../01_M0_M2_FOUNDATION_AND_AUTH.md | 3 +- docs/technical/README.md | 1 + ...M_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md | 4 + ..._SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md | 364 ++++++++++++++++++ server-rs/packages/module-auth/README.md | 3 +- 5 files changed, 373 insertions(+), 2 deletions(-) create mode 100644 docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md diff --git a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md index 4d7e982c..6eacd735 100644 --- a/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md +++ b/backend-rewrite-tasklist/01_M0_M2_FOUNDATION_AND_AUTH.md @@ -150,7 +150,8 @@ 交付物:[../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_AUTH_AUDIT_LOG_TABLE_DESIGN_2026-04-21.md) - [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` +- [x] 设计 `sms_auth_event` + 交付物:[../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md) - [ ] 设计 `wechat_auth_state` ### Axum 鉴权服务 diff --git a/docs/technical/README.md b/docs/technical/README.md index 3304fe17..80fafa76 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -4,6 +4,7 @@ ## 文档列表 +- [SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md):`M2` 第六张短信鉴权统计表 `sms_auth_event` 的事件范围、统计口径、索引与和风控/审计表的协作边界。 - [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 边界、轮换与吊销语义、索引与迁移规则。 diff --git a/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md b/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md index d680eca6..c9ea7393 100644 --- a/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md +++ b/docs/technical/SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md @@ -326,6 +326,10 @@ server-rs/ - [SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_RISK_BLOCK_TABLE_DESIGN_2026-04-21.md) +`sms_auth_event` 的事件范围、发送/校验写入规则、统计口径与和风控/审计表的边界,见: + +- [SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md) + ### B. 运行时主状态表 - `runtime_snapshot` diff --git a/docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md b/docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md new file mode 100644 index 00000000..2c038eee --- /dev/null +++ b/docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md @@ -0,0 +1,364 @@ +# `sms_auth_event` 表设计 + +日期:`2026-04-21` + +## 1. 文档目的 + +这份文档用于完成 `M2` 的第六条任务:`设计 sms_auth_event`。 + +目标是把短信鉴权事件表固定成一张“短信发送与验证码校验的统计源表”,并明确: + +1. 哪些短信相关动作要写入事件 +2. 哪些动作不应该写进这张表 +3. 发送频控、失败次数限制、captcha 触发、风险保护分别依赖什么统计口径 +4. 它与 `auth_risk_block`、`auth_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` | 是 | 业务场景,枚举固定为 `login`、`bind_phone`、`change_phone`。 | +| `action` | `String` | 是 | 动作类型,枚举固定为 `send_code`、`verify_code`。 | +| `success` | `bool` | 是 | 当前动作是否成功。 | +| `ip` | `Option` | 否 | 请求来源 IP;缺失时为 `null`。 | +| `user_agent` | `Option` | 否 | 请求来源 UA;缺失时为 `null`。 | +| `created_at` | `String` | 是 | 事件发生时间,UTC RFC3339。 | + +补充约束: + +1. 当前阶段不存 `provider_request_id`,因为这张表不是供应商排障日志表。 +2. 当前阶段不存 `provider_code`、`provider_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_required`、`risk_block_phone`、`risk_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. 组织 `scene`、`action`、`success` +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_block`、`auth_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` diff --git a/server-rs/packages/module-auth/README.md b/server-rs/packages/module-auth/README.md index 4acc07b5..0f9ffadb 100644 --- a/server-rs/packages/module-auth/README.md +++ b/server-rs/packages/module-auth/README.md @@ -13,7 +13,7 @@ ## 2. 当前阶段说明 -当前阶段已先冻结第一张账号主表 `user_account` 的设计,其余身份表、会话表与 token 细节仍按顺序继续展开。 +当前阶段已冻结前六张鉴权基础表设计,剩余 `wechat_auth_state` 与 token 细节仍按顺序继续展开。 后续与本 package 直接相关的任务包括: @@ -29,6 +29,7 @@ 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) +6. [../../../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md](../../../docs/technical/SPACETIMEDB_SMS_AUTH_EVENT_TABLE_DESIGN_2026-04-21.md) ## 3. 边界约束