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; + } + +}