feat(SysUserServiceImpl): 添加短信验证码校验逻辑,防止重复使用确保验证码一次性消费

- 更新用户注册和找回密码逻辑,使用新的验证码校验方法
- 添加 SysUserSmsCodeTests 测试类,验证验证码消费行为
This commit is contained in:
PIGCLOUD committed 2026-11-05 16:36:32 +08:00
1 parent fd5cd572a4
commit db23fd2d0c
2 files changed
+99 -9

No files matched your search

@@ -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<SysUserMapper, SysUser> impl
private final SysDeptMapper sysDeptMapper;
private final CaptchaValidator captchaValidator;
/**
* 保存用户信息
*
@@ -332,9 +335,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> 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<SysUserMapper, SysUser> 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<SysUserMapper, SysUser> 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<SysUserMapper, SysUser> 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列表
*
@@ -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<String, Object> 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<String, Object> redis = mock(RedisTemplate.class);
ValueOperations<String, Object> 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;
}
}