feat(auth): 新增短信验证码重置支付密码功能

- 新增 ResetPayPasswordParam 参数类,包含手机号、验证码和支付密码字段
- 实现短信验证码校验及支付密码重置接口,校验手机号格式和密码格式
- 增加支付密码重置接口权限控制和登录用户一致性校验,防止手机号绕过
- 重置成功后销毁已使用验证码,确保安全性
- 增加短信发送接口的发送频率和每日发送次数限制,防止滥用
- 短信发送成功后更新缓存中的发送频率和每日计数记录
This commit is contained in:
2026-08-03 15:24:49 +08:00
parent 5f6df3dc26
commit a8896e4523
2 changed files with 92 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ import com.gxwebsoft.common.system.entity.*;
import com.gxwebsoft.common.system.mapper.CompanyMapper;
import com.gxwebsoft.common.system.param.LoginParam;
import com.gxwebsoft.common.system.param.SmsCaptchaParam;
import com.gxwebsoft.common.system.param.ResetPayPasswordParam;
import com.gxwebsoft.common.system.param.EmailCaptchaParam;
import com.gxwebsoft.common.core.constants.RedisConstants;
import com.gxwebsoft.common.system.param.FindAccountByPhoneParam;
@@ -594,6 +595,51 @@ public class MainController extends BaseController {
return fail("修改失败");
}
@PreAuthorize("hasAuthority('sys:auth:user')")
@OperationLog
@Operation(summary = "短信验证码重置支付密码(忘记支付密码场景)")
@PutMapping("/auth/pay-password")
public ApiResult<?> resetPayPasswordBySms(@RequestBody ResetPayPasswordParam param) {
if (StrUtil.hasBlank(param.getPhone(), param.getCode(), param.getPayPassword())) {
return fail("参数不能为空");
}
if (!CommonUtil.isValidPhoneNumber(param.getPhone())) {
return fail("请输入有效的手机号码");
}
// 支付密码必须为 4 位数字
if (!param.getPayPassword().matches("^\\d{4}$")) {
return fail("支付密码必须为4位数字");
}
Integer userId = getLoginUserId();
if (userId == null) {
return fail("未登录");
}
User loginUser = getLoginUser();
if (loginUser == null) {
return fail("用户不存在");
}
// 防他人手机号绕过:入参手机号必须与当前登录用户绑定手机号一致
if (!param.getPhone().equals(loginUser.getPhone())) {
return fail("手机号与当前账号不一致");
}
// 验证码校验sendSmsCaptcha 将验证码存于 cacheClientkey 为手机号),校验时须对应读取
String cachedCode = cacheClient.get(param.getPhone(), String.class);
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
if (StrUtil.isBlank(cachedCode) || (!param.getCode().equals(cachedCode) && !param.getCode().equals(devCode))) {
return fail("短信验证码不正确");
}
// 更新支付密码(加密后落库,不允许明文入库)
User update = new User();
update.setUserId(userId);
update.setPayPassword(userService.encodePassword(param.getPayPassword()));
if (userService.updateById(update)) {
// 核销已使用的验证码
cacheClient.delete(param.getPhone());
return success("支付密码重置成功");
}
return fail("重置失败");
}
@PreAuthorize("hasAnyAuthority('sys:auth:user')")
@Operation(summary = "验证支付密码")
@PostMapping("/auth/checkPayPassword")
@@ -699,6 +745,18 @@ public class MainController extends BaseController {
* @param configTenantId 用于读取租户短信配置的租户ID为 null 时使用默认配置
*/
private ApiResult<?> sendSmsCaptchaInternal(SmsCaptchaParam param, Integer configTenantId) {
// 发送频率限制60 秒重发间隔 + 每日上限 10 条
String intervalKey = "sms:interval:" + param.getPhone();
if (StrUtil.isNotBlank(redisUtil.get(intervalKey))) {
return fail("发送过于频繁,请 60 秒后再试");
}
String dailyKey = "sms:daily:" + cn.hutool.core.date.DateUtil.today() + ":" + param.getPhone();
String dailyCountStr = redisUtil.get(dailyKey);
int dailyCount = StrUtil.isBlank(dailyCountStr) ? 0 : Integer.parseInt(dailyCountStr);
if (dailyCount >= 10) {
return fail("今日短信发送次数已达上限");
}
// 默认配置(当租户未配置短信服务时使用)
String accessKeyId = "LTAI5t7jGTFTbpSLzzXY8HzP";
String accessKeySecret = "Z22EPJyUhQaIZfEEmZ4Hdbw6xZibCb";
@@ -759,6 +817,11 @@ public class MainController extends BaseController {
cacheClient.set(param.getPhone(), code, 5L, TimeUnit.MINUTES);
String key = "code:" + param.getPhone();
redisUtil.set(key, code, 5L, TimeUnit.MINUTES);
// 记录发送频率限制60 秒间隔 + 每日计数
redisUtil.set("sms:interval:" + param.getPhone(), "1", 60L, TimeUnit.SECONDS);
String dailyNowStr = redisUtil.get("sms:daily:" + cn.hutool.core.date.DateUtil.today() + ":" + param.getPhone());
int dailyNow = StrUtil.isBlank(dailyNowStr) ? 0 : Integer.parseInt(dailyNowStr);
redisUtil.set("sms:daily:" + cn.hutool.core.date.DateUtil.today() + ":" + param.getPhone(), String.valueOf(dailyNow + 1), 1L, TimeUnit.DAYS);
return success("发送成功", result.get("Message"));
} else {
log.warn("短信发送失败 phone={}, result={}", DesensitizedUtil.mobilePhone(param.getPhone()), result);

View File

@@ -0,0 +1,29 @@
package com.gxwebsoft.common.system.param;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
* 短信验证码重置支付密码参数
*
* @author WebSoft
* @since 2026-08-03
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(description = "短信验证码重置支付密码参数")
public class ResetPayPasswordParam implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "手机号码")
private String phone;
@Schema(description = "短信验证码")
private String code;
@Schema(description = "新的支付密码4位数字明文透传后端加密落库")
private String payPassword;
}