From a8896e4523cd3cd7536cc1b9ea45bccad34dd246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Mon, 3 Aug 2026 15:24:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E6=96=B0=E5=A2=9E=E7=9F=AD?= =?UTF-8?q?=E4=BF=A1=E9=AA=8C=E8=AF=81=E7=A0=81=E9=87=8D=E7=BD=AE=E6=94=AF?= =?UTF-8?q?=E4=BB=98=E5=AF=86=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ResetPayPasswordParam 参数类,包含手机号、验证码和支付密码字段 - 实现短信验证码校验及支付密码重置接口,校验手机号格式和密码格式 - 增加支付密码重置接口权限控制和登录用户一致性校验,防止手机号绕过 - 重置成功后销毁已使用验证码,确保安全性 - 增加短信发送接口的发送频率和每日发送次数限制,防止滥用 - 短信发送成功后更新缓存中的发送频率和每日计数记录 --- .../system/controller/MainController.java | 63 +++++++++++++++++++ .../system/param/ResetPayPasswordParam.java | 29 +++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/main/java/com/gxwebsoft/common/system/param/ResetPayPasswordParam.java diff --git a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java index 035da10..9391456 100644 --- a/src/main/java/com/gxwebsoft/common/system/controller/MainController.java +++ b/src/main/java/com/gxwebsoft/common/system/controller/MainController.java @@ -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); diff --git a/src/main/java/com/gxwebsoft/common/system/param/ResetPayPasswordParam.java b/src/main/java/com/gxwebsoft/common/system/param/ResetPayPasswordParam.java new file mode 100644 index 0000000..3aea9cc --- /dev/null +++ b/src/main/java/com/gxwebsoft/common/system/param/ResetPayPasswordParam.java @@ -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; +}