feat(auth): 新增短信验证码重置支付密码功能
- 新增 ResetPayPasswordParam 参数类,包含手机号、验证码和支付密码字段 - 实现短信验证码校验及支付密码重置接口,校验手机号格式和密码格式 - 增加支付密码重置接口权限控制和登录用户一致性校验,防止手机号绕过 - 重置成功后销毁已使用验证码,确保安全性 - 增加短信发送接口的发送频率和每日发送次数限制,防止滥用 - 短信发送成功后更新缓存中的发送频率和每日计数记录
This commit is contained in:
@@ -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 将验证码存于 cacheClient(key 为手机号),校验时须对应读取
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user