From db23fd2d0c831491f25aa6cfe4e51269424e27fd Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Thu, 5 Nov 2026 16:21:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(SysUserServiceImpl):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=9F=AD=E4=BF=A1=E9=AA=8C=E8=AF=81=E7=A0=81=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E9=98=B2=E6=AD=A2=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=A1=AE=E4=BF=9D=E9=AA=8C=E8=AF=81=E7=A0=81?= =?UTF-8?q?=E4=B8=80=E6=AC=A1=E6=80=A7=E6=B6=88=E8=B4=B9=20-=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=94=A8=E6=88=B7=E6=B3=A8=E5=86=8C=E5=92=8C=E6=89=BE?= =?UTF-8?q?=E5=9B=9E=E5=AF=86=E7=A0=81=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=96=B0=E7=9A=84=E9=AA=8C=E8=AF=81=E7=A0=81=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E6=96=B9=E6=B3=95=20-=20=E6=B7=BB=E5=8A=A0=20SysUserS?= =?UTF-8?q?msCodeTests=20=E6=B5=8B=E8=AF=95=E7=B1=BB=EF=BC=8C=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E9=AA=8C=E8=AF=81=E7=A0=81=E6=B6=88=E8=B4=B9=E8=A1=8C?= =?UTF-8?q?=E4=B8=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/impl/SysUserServiceImpl.java | 26 ++++-- .../service/impl/SysUserSmsCodeTests.java | 82 +++++++++++++++++++ 2 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysUserSmsCodeTests.java diff --git a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysUserServiceImpl.java b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysUserServiceImpl.java index c502f890c..247bf4f04 100644 --- a/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysUserServiceImpl.java +++ b/pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/SysUserServiceImpl.java @@ -49,6 +49,7 @@ import com.pig4cloud.pigx.common.data.datascope.DataScope; import com.pig4cloud.pigx.common.data.resolver.ParamResolver; import com.pig4cloud.pigx.common.data.tenant.TenantBroker; import com.pig4cloud.pigx.common.excel.vo.ErrorMessage; +import com.pig4cloud.pigx.common.security.captcha.CaptchaValidator; import com.pig4cloud.pigx.common.security.service.PigxUser; import com.pig4cloud.pigx.common.security.util.SecurityUtils; import lombok.AllArgsConstructor; @@ -110,6 +111,8 @@ public class SysUserServiceImpl extends ServiceImpl impl private final SysDeptMapper sysDeptMapper; + private final CaptchaValidator captchaValidator; + /** * 保存用户信息 * @@ -332,9 +335,7 @@ public class SysUserServiceImpl extends ServiceImpl impl // 校验手机号 if (StrUtil.isNotBlank(userDto.getPhone())) { - Object codeObj = RedisUtils.get(CacheConstants.SMS_CODE_KEY + LoginTypeEnum.SMS.getType() - + StringPool.AT + userDto.getPhone()); - if (!StrUtil.equals(userDto.getCode(), codeObj.toString())) { + if (!validateSmsCode(userDto.getPhone(), userDto.getCode())) { return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_ERROR)); } sysUser.setPhone(userDto.getPhone()); @@ -607,9 +608,7 @@ public class SysUserServiceImpl extends ServiceImpl impl if (StrUtil.isBlank(userDto.getCode())) { return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_PARAM_ILLEGAL)); } - String codeObj = RedisUtils - .get(CacheConstants.SMS_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + userDto.getPhone()); - if (!StrUtil.equals(codeObj, userDto.getCode())) { + if (!validateSmsCode(userDto.getPhone(), userDto.getCode())) { return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_ERROR)); } } @@ -772,9 +771,7 @@ public class SysUserServiceImpl extends ServiceImpl impl return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_PARAM_ILLEGAL)); } - String codeObj = RedisUtils - .get(CacheConstants.SMS_CODE_KEY + LoginTypeEnum.SMS.getType() + StringPool.AT + userDto.getPhone()); - if (!StrUtil.equals(codeObj, code)) { + if (!validateSmsCode(userDto.getPhone(), code)) { return R.failed(MsgUtils.getMessage(UpmsErrorCodes.SYS_APP_SMS_ERROR)); } @@ -798,6 +795,17 @@ public class SysUserServiceImpl extends ServiceImpl impl return R.ok(); } + /** + * 校验短信验证码,读取即作废,防止同一验证码被重复尝试爆破 + * + * @param phone 手机号 + * @param code 短信验证码 + * @return 校验通过返回 true + */ + private boolean validateSmsCode(String phone, String code) { + return captchaValidator.validateSms(LoginTypeEnum.SMS.getType() + StringPool.AT + phone, code).isOk(); + } + /** * 根据角色ID列表获取用户ID列表 * diff --git a/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysUserSmsCodeTests.java b/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysUserSmsCodeTests.java new file mode 100644 index 000000000..504dd9ad2 --- /dev/null +++ b/pigx-upms/pigx-upms-biz/src/test/java/com/pig4cloud/pigx/admin/service/impl/SysUserSmsCodeTests.java @@ -0,0 +1,82 @@ +package com.pig4cloud.pigx.admin.service.impl; + +import com.pig4cloud.pigx.admin.api.dto.RegisterUserDTO; +import com.pig4cloud.pigx.common.core.util.SpringContextHolder; +import com.pig4cloud.pigx.common.security.captcha.CaptchaValidator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.support.StaticMessageSource; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 短信验证码必须一次性消费,防止 4 位验证码被重复尝试爆破。 + */ +class SysUserSmsCodeTests { + + private static final String KEY = "SMS_CODE_KEY:SMS@13800138000"; + + private final Map cache = new HashMap<>(); + + @AfterEach + void cleanup() { + RequestContextHolder.resetRequestAttributes(); + new SpringContextHolder().postProcessBeanFactory(null); + SpringContextHolder.clearHolder(); + } + + @Test + void wrongCodeConsumesSmsCodeForRegisterAndForgetPassword() { + SysUserServiceImpl service = newService(); + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest())); + + cache.put(KEY, "1234"); + assertThat(service.registerUser(dto("0000")).isOk()).isFalse(); + // 猜错一次后验证码已作废,正确的码也不能再用 + assertThat(service.registerUser(dto("1234")).isOk()).isFalse(); + assertThat(cache).doesNotContainKey(KEY); + + cache.put(KEY, "1234"); + assertThat(service.forgetUserPassword(dto(null), "0000").isOk()).isFalse(); + assertThat(service.forgetUserPassword(dto(null), "1234").isOk()).isFalse(); + assertThat(cache).doesNotContainKey(KEY); + } + + @SuppressWarnings("unchecked") + private SysUserServiceImpl newService() { + RedisTemplate redis = mock(RedisTemplate.class); + ValueOperations values = mock(ValueOperations.class); + when(redis.opsForValue()).thenReturn(values); + when(values.getAndDelete(anyString())).thenAnswer(invocation -> cache.remove(invocation.getArgument(0))); + + DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); + StaticMessageSource messages = new StaticMessageSource(); + messages.setUseCodeAsDefaultMessage(true); + factory.registerSingleton("pigxMessageSource", messages); + new SpringContextHolder().postProcessBeanFactory(factory); + + return new SysUserServiceImpl(null, null, null, null, null, null, null, null, null, null, null, null, + new CaptchaValidator(null, redis)); + } + + private RegisterUserDTO dto(String code) { + RegisterUserDTO dto = new RegisterUserDTO(); + dto.setUsername("attacker"); + dto.setPhone("13800138000"); + dto.setCode(code); + return dto; + } + +}