diff --git a/changelogs/2026-05/22_feat_admin_sms_login.md b/changelogs/2026-05/22_feat_admin_sms_login.md new file mode 100644 index 0000000..316945e --- /dev/null +++ b/changelogs/2026-05/22_feat_admin_sms_login.md @@ -0,0 +1,197 @@ +# feat(user): 管理端手机号验证码登录(短信路径免 2FA) + +> **仓库**: HL (后端 hl-user-service + hl-gateway) +> **关联 PR/Issue**: PR #2904, Closes #2889 +> **日期**: 2026-05-22 +> **影响范围**: 管理后台登录页加「短信验证码登录」入口 +> **接收方**: mmg (前端) +> **前端**: **需要改动 — 登录页加 tab + 手机号/验证码输入 + 倒计时按钮** + +--- + +## 🚨 关键变化 + +新增 2 个接口,管理员可以用「手机号 + 短信验证码」登录后台,**短信路径本身不再走企业微信 2FA**(账号密码登录路径完全不动,仍保留 2FA)。 + +依赖前置工单 #2886 的 `admin_user.mobile` 字段(已上线)。 + +--- + +## 一、API 影响 + +### 新增 `POST /admin/auth/sms/send-code` 发送验证码 + +请求体: + +```json +{ + "mobile": "13800138000" +} +``` + +| 字段 | 类型 | 必填 | 校验 | +|------|------|-----|------| +| mobile | String | ✅ | `^1[3-9]\d{9}$` 11 位 | + +响应: + +| 场景 | code | message | +|------|------|---------| +| 注册 mobile 真发 | 200 | 成功 | +| **未注册 mobile**(防枚举) | **200** | **成功**(静默不真发,但日上限消费) | +| 60s 内重发 | 200105 | 发送过于频繁,请 60 秒后重试 | +| 当日 >= 10 次 | 200106 | 今日发送次数已达上限 | +| mobile 格式错 / 漏 | 400 | 手机号不能为空 / 手机号格式不正确 | + +⚠️ **防枚举关键**: 未注册 mobile **也返 200** 跟"已注册"路径**外部不可区分**(末尾 300-800ms 随机 jitter 抹平时延)。前端不能依据 send-code 响应判断 mobile 是否注册。 + +### 新增 `POST /admin/auth/sms/login` 验证码登录 + +请求体: + +```json +{ + "mobile": "13800138000", + "code": "123456" +} +``` + +| 字段 | 类型 | 必填 | 校验 | +|------|------|-----|------| +| mobile | String | ✅ | `^1[3-9]\d{9}$` | +| code | String | ✅ | `^\d{6}$` 6 位数字 | + +响应: + +| 场景 | code | 说明 | +|------|------|------| +| 登录成功 | 200 | 返回 data = AdminLoginResponse(含 token, refreshToken, adminId, username, role, roleName, avatar, roles, **`requireTwoFa: false`**) | +| 验证码错 | 200102 | 验证码错误(fail+1) | +| 验证码过期/未发送 | 200103 | 验证码已过期,请重新获取(**fail 不增**) | +| 失败 >= 5 锁定 | 200104 | 短信登录失败次数过多,请 15 分钟后重试 | +| 对码 + mobile 不绑 admin | 200102 | 验证码错误(**防枚举,假装码错不报"用户不存在"**) | + +⚠️ **`requireTwoFa: false` 是工单核心放行点**: 短信验证码本身是「拥有手机」的二次因子,与企微 2FA 等价,不再叠加。**账号密码登录的 requireTwoFa=true 行为完全不变**。 + +--- + +## 🚨 前端需要做的事 + +### 1. 登录页加「短信验证码登录」tab + +文件: `hl-ui/src/views/login/...` (登录页组件) + +跟现有「账号密码」「企业微信扫码」并列,加一个新 tab: + +``` +[ 账号密码 ] [ 企业微信扫码 ] [ 短信验证码 ] +``` + +### 2. 「短信验证码」表单 + +- 输入框 1: 手机号(11 位格式预校验 `^1[3-9]\d{9}$`) +- 按钮: 「获取验证码」点击 → POST `/admin/auth/sms/send-code` + - 点击后 60s 倒计时(按钮文案 `60s 后重新获取`),复用现有小程序短信发送倒计时组件 + - 响应 200 → 提示「验证码已发送」(**注册/未注册都提示已发送,不能透露 mobile 是否绑**) + - 响应 200105 → 提示「发送过于频繁,请稍后重试」 + - 响应 200106 → 提示「今日发送次数已达上限」 +- 输入框 2: 验证码(6 位数字) +- 按钮:「登录」点击 → POST `/admin/auth/sms/login` + - 响应 200 + data → 走现有登录成功流程(存 token / 跳转,**不再触发 2FA 弹窗**) + - 响应 200102 → 提示「验证码错误」 + - 响应 200103 → 提示「验证码已过期,请重新获取」并清空验证码输入 + - 响应 200104 → 提示「失败次数过多,请 15 分钟后重试」并锁登录按钮 15min + +### 3. 入口指引(可选) + +在「账号密码」tab 下方加小字提示: + +``` +忘记密码? 试试 [短信验证码登录] +``` + +### 4. 自查 + +```bash +grep -rE "admin.*sms|短信验证码登录" hl-ui/src +``` + +--- + +## 二、错误码总览(本工单新增 5 个) + +| code | 含义 | +|------|------| +| 200102 | 验证码错误 / 对码但 mobile 不绑 admin | +| 200103 | 验证码已过期 / 未发送 | +| 200104 | 短信登录失败次数过多(15 分钟锁定) | +| 200105 | 发送过于频繁(60s 间隔) | +| 200106 | 今日发送次数已达上限(10/d) | + +--- + +## 三、安全设计 + +### 4 重防枚举攻击 + +1. 间隔锁 60s + 日上限 10/d **先于** admin 查询消费(未注册 mobile 也扣额度,防刷量) +2. 未注册 mobile **静默返 200** 不真发不存 code +3. 末尾 **300-800ms 随机 jitter** 防时间侧信道(注册/未注册路径外部时延无差异) +4. login 阶段 mobile 不绑 admin **即使码对也返 INVALID** 跟真错码路径不可区分 + +### Redis namespace 隔离 + +跟小程序端 `sms:*` 完全隔离,本接口用: + +- `admin:sms:code:{mobile}` TTL 300s +- `admin:sms:interval:{mobile}` TTL 60s +- `admin:sms:limit:{mobile}` TTL 到当日午夜 +- `admin:sms:fail:{mobile}` TTL 900s (15min) + +### 失败计数策略 + +- 用 Redis `admin:sms:fail:{mobile}` 独立计数(**不动 `admin_user.failed_login_count`**,后者密码场景专用) +- 验证码错累加 → 累计 5 次锁 15 分钟 +- 验证码过期/未发送**不累加**(防攻击者用别人 mobile 试码锁住真用户) + +--- + +## 四、DB 改动 + +`hl-user-service`: + +- `V20260523_002__login_log_method_to_varchar.sql`: `login_log.login_method` 从 ENUM(4 值) 改 `VARCHAR(32)`,新增 `SMS` 值,后续登录方式扩展无需改 DDL + +--- + +## 五、测试服验证记录 + +部署到测试服 (`https://web.test.1814.love:9443` 网关 + `hl-user-service` 双实例) 后已 round-trip 验证 (2026-05-22 16:55): + +| # | 场景 | 预期 | 实测 | +|---|------|------|------| +| T1 | send-code 注册 mobile | 200 + Redis 存 code | ✅ code='505310' | +| T2 | 60s 重发 | 200105 | ✅ | +| T3 | send-code 未注册 mobile | 200 静默 Redis 无 code | ✅ 防枚举 | +| T4 | login 对码 | 200 + token + role + **requireTwoFa=False** | ✅ | +| T7 | 对码 + mobile 不绑 admin | 200102 INVALID | ✅ 防枚举 | +| T8 | 5 次错码累加锁定 | fail 1→5, 第 6 次 200104 | ✅ | + +测试服 user 双实例 + gateway 双实例都已部署 dev 最新,可联调。 + +--- + +## 六、不在本期范围 + +- 「忘记密码走短信重置」(另起工单) +- 完全废弃企微 2FA (账密登录链路保留 2FA) +- 注册 mobile 检查接口(防枚举设计不暴露) + +--- + +## 七、联系人 + +后端: wx(呼籁旅行) +前端: mmg + +如有疑问可在 [#2889](https://git.1814.love:8443/wx/HL/issues/2889) 评论区留言。