hl-api-changelog/changelogs/2026-05/22_feat_admin_sms_login.md

6.6 KiB

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 发送验证码

请求体:

{
  "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 验证码登录

请求体:

{
  "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. 自查

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 评论区留言。