fix(app): 修复 AppController 安全与查询问题
- 加强异常处理、类型安全和图片上传校验 - 优化设备相关查询,避免重复访问数据源 - 补充并记录并发测试与审查修复实施计划
This commit is contained in:
@@ -67,26 +67,32 @@ public class CaptchaController {
|
||||
@RateLimiter(key = "#username", time = 60, count = 1)
|
||||
@GetMapping("/resource/code")
|
||||
public R<Void> code(@NotBlank(message = "{user.username.not.blank}") String username) {
|
||||
String key = GlobalConstants.CAPTCHA_CODE_KEY + username;
|
||||
String code = RandomUtil.randomNumbers(6);
|
||||
RedisUtils.setCacheObject(key, code, Duration.ofMinutes(Constants.CAPTCHA_EXPIRATION));
|
||||
if (Validator.isMobile(username)) {
|
||||
return sendSmsCode(username, username);
|
||||
}
|
||||
if (Validator.isEmail(username)) {
|
||||
return sendEmailCode(username, username);
|
||||
}
|
||||
|
||||
if (Validator.isMobile(username)){
|
||||
// 验证码模板id 自行处理 (查数据库或写死均可)
|
||||
String templateId = "SMS_333877107";
|
||||
LinkedHashMap<String, String> map = new LinkedHashMap<>(1);
|
||||
map.put("code", code);
|
||||
SmsBlend smsBlend = SmsFactory.getSmsBlend("config1");
|
||||
SmsResponse smsResponse = smsBlend.sendMessage(username, templateId, map);
|
||||
if (!smsResponse.isSuccess()) {
|
||||
log.error("验证码短信发送异常 => {}", smsResponse);
|
||||
return R.fail(smsResponse.getData().toString());
|
||||
}
|
||||
}else {
|
||||
emailCodeImpl(username);
|
||||
}
|
||||
SysUserVo user = userService.selectUserByUserName(username);
|
||||
if (user == null) {
|
||||
return R.fail("账号未注册");
|
||||
}
|
||||
if (Validator.isMobile(user.getPhonenumber())) {
|
||||
return sendSmsCode(user.getPhonenumber(), username);
|
||||
}
|
||||
if (Validator.isEmail(user.getEmail())) {
|
||||
return sendEmailCode(user.getEmail(), username);
|
||||
}
|
||||
return R.fail("当前账号未绑定手机号或邮箱");
|
||||
}
|
||||
|
||||
return R.ok(code);
|
||||
private R<Void> sendEmailCode(String email, String cacheKey) {
|
||||
if (!mailProperties.getEnabled()) {
|
||||
return R.fail("当前系统没有开启邮箱功能!");
|
||||
}
|
||||
emailCodeImpl(email, cacheKey);
|
||||
return R.ok("操作成功");
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
import cn.hutool.core.lang.Validator;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.domain.model.ForgotLoginBody;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.domain.vo.SysUserVo;
|
||||
import org.dromara.system.mapper.SysUserMapper;
|
||||
import org.dromara.system.service.ISysUserService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
|
||||
@@ -22,41 +11,14 @@ import org.springframework.stereotype.Service;
|
||||
@Service
|
||||
public class ForgotPasswordService {
|
||||
|
||||
private final SysUserMapper userMapper;
|
||||
private final ISysUserService userService;
|
||||
|
||||
|
||||
public boolean forgotPasswordService(ForgotLoginBody loginBody) {
|
||||
SysUserVo appUserVo =new SysUserVo();
|
||||
if(Validator.isEmail(loginBody.getUsername())){
|
||||
userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getEmail, loginBody.getUsername()));
|
||||
}else if (Validator.isMobile(loginBody.getUsername())){
|
||||
appUserVo = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getPhonenumber, loginBody.getUsername()));
|
||||
}else {
|
||||
appUserVo = userMapper.selectVoOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUserName, loginBody.getUsername()));
|
||||
}
|
||||
if (ObjectUtil.isNull(appUserVo)){
|
||||
throw new UserException("账号未注册");
|
||||
}
|
||||
|
||||
boolean validateFlag = validateSmsCode(loginBody.getUsername(), loginBody.getSmsCode());
|
||||
if (!validateFlag){
|
||||
throw new UserException("验证码无效");
|
||||
}
|
||||
|
||||
appUserVo.setPassword(BCrypt.hashpw(loginBody.getPassword()));
|
||||
SysUser update = MapstructUtils.convert(appUserVo, SysUser.class);
|
||||
userMapper.updateById(update);
|
||||
return userMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验短信验证码
|
||||
*/
|
||||
private boolean validateSmsCode(String username, String smsCode) {
|
||||
String code = RedisUtils.getCacheObject(GlobalConstants.CAPTCHA_CODE_KEY + username);
|
||||
if (StringUtils.isBlank(code)) {
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
return code.equals(smsCode);
|
||||
String verificationCode = StringUtils.isNotBlank(loginBody.getSmsCode())
|
||||
? loginBody.getSmsCode()
|
||||
: loginBody.getCode();
|
||||
return userService.resetPasswordByVerificationCode(
|
||||
loginBody.getUsername(), verificationCode, loginBody.getPassword());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ security:
|
||||
- /*/api-docs
|
||||
- /*/api-docs/**
|
||||
- /warm-flow-ui/config
|
||||
- /app/v1/retrievePassword
|
||||
|
||||
# 多租户配置
|
||||
tenant:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.dromara.web.config;
|
||||
|
||||
import org.dromara.common.security.config.SecurityConfig;
|
||||
import org.dromara.common.security.config.properties.SecurityProperties;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
|
||||
import org.springframework.boot.env.YamlPropertySourceLoader;
|
||||
import org.springframework.core.env.MutablePropertySources;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.handler.MappedInterceptor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Tag("dev")
|
||||
class SecurityExcludesConfigUnitTest {
|
||||
|
||||
@Test
|
||||
void retrievePassword_isExcludedFromLoginAuthentication() throws Exception {
|
||||
List<PropertySource<?>> sources = new YamlPropertySourceLoader()
|
||||
.load("application", new ClassPathResource("application.yml"));
|
||||
MutablePropertySources propertySources = new MutablePropertySources();
|
||||
for (PropertySource<?> source : sources) {
|
||||
propertySources.addLast(source);
|
||||
}
|
||||
SecurityProperties properties = new Binder(ConfigurationPropertySources.from(propertySources))
|
||||
.bind("security", Bindable.of(SecurityProperties.class))
|
||||
.orElseThrow(() -> new AssertionError("security 配置未绑定"));
|
||||
|
||||
assertThat(properties.getExcludes()).contains("/app/v1/retrievePassword");
|
||||
}
|
||||
|
||||
@Test
|
||||
void securityInterceptor_alwaysExcludesRetrievePasswordWithoutExternalConfiguration() {
|
||||
SecurityProperties properties = new SecurityProperties();
|
||||
properties.setExcludes(new String[0]);
|
||||
SecurityConfig config = new SecurityConfig(properties);
|
||||
ReflectionTestUtils.setField(config, "ssePath", "/resource/sse");
|
||||
ExposedInterceptorRegistry registry = new ExposedInterceptorRegistry();
|
||||
|
||||
config.addInterceptors(registry);
|
||||
|
||||
MappedInterceptor interceptor = (MappedInterceptor) registry.getRegisteredInterceptors().get(0);
|
||||
assertThat(interceptor.getExcludePathPatterns()).contains("/app/v1/retrievePassword");
|
||||
}
|
||||
|
||||
private static final class ExposedInterceptorRegistry extends InterceptorRegistry {
|
||||
|
||||
private List<Object> getRegisteredInterceptors() {
|
||||
return super.getInterceptors();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,52 @@ class CaptchaControllerUnitTest {
|
||||
@Mock
|
||||
private ISysUserService userService;
|
||||
|
||||
@Test
|
||||
void code_resolvesAccountPhoneAndUsesAccountAsCacheKey() {
|
||||
CaptchaController controller = spy(new CaptchaController(null, mailProperties, userService));
|
||||
SysUserVo user = new SysUserVo();
|
||||
user.setPhonenumber("13305376054");
|
||||
when(userService.selectUserByUserName("alice")).thenReturn(user);
|
||||
doReturn(R.ok("操作成功")).when(controller).sendSmsCode("13305376054", "alice");
|
||||
|
||||
R<Void> result = controller.code("alice");
|
||||
|
||||
assertThat(result.getCode()).isEqualTo(200);
|
||||
assertThat(result.getData()).isNull();
|
||||
verify(userService).selectUserByUserName("alice");
|
||||
verify(controller).sendSmsCode("13305376054", "alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void code_resolvesAccountEmailAndUsesAccountAsCacheKey() {
|
||||
CaptchaController controller = spy(new CaptchaController(null, mailProperties, userService));
|
||||
SysUserVo user = new SysUserVo();
|
||||
user.setEmail("alice@example.com");
|
||||
when(userService.selectUserByUserName("alice")).thenReturn(user);
|
||||
when(mailProperties.getEnabled()).thenReturn(true);
|
||||
doNothing().when(controller).emailCodeImpl("alice@example.com", "alice");
|
||||
|
||||
R<Void> result = controller.code("alice");
|
||||
|
||||
assertThat(result.getCode()).isEqualTo(200);
|
||||
assertThat(result.getData()).isNull();
|
||||
verify(userService).selectUserByUserName("alice");
|
||||
verify(controller).emailCodeImpl("alice@example.com", "alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void code_keepsDirectMobileBehaviorWithoutReturningPlainCode() {
|
||||
CaptchaController controller = spy(new CaptchaController(null, mailProperties, userService));
|
||||
doReturn(R.ok("操作成功")).when(controller).sendSmsCode("13305376054", "13305376054");
|
||||
|
||||
R<Void> result = controller.code("13305376054");
|
||||
|
||||
assertThat(result.getCode()).isEqualTo(200);
|
||||
assertThat(result.getData()).isNull();
|
||||
verify(controller).sendSmsCode("13305376054", "13305376054");
|
||||
verifyNoInteractions(userService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void accountCancelCode_sendsSmsCodeWhenCurrentUserHasPhone() {
|
||||
CaptchaController controller = spy(new CaptchaController(null, mailProperties, userService));
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.dromara.web.service;
|
||||
|
||||
import org.dromara.common.core.domain.model.ForgotLoginBody;
|
||||
import org.dromara.system.service.ISysUserService;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@Tag("dev")
|
||||
class ForgotPasswordServiceTest {
|
||||
|
||||
@Mock
|
||||
private ISysUserService userService;
|
||||
@InjectMocks
|
||||
private ForgotPasswordService forgotPasswordService;
|
||||
|
||||
@Test
|
||||
void forgotPasswordService_delegatesVerificationAndResetToUserService() {
|
||||
ForgotLoginBody body = new ForgotLoginBody();
|
||||
body.setUsername("alice");
|
||||
body.setSmsCode("123456");
|
||||
body.setPassword("newPassword");
|
||||
when(userService.resetPasswordByVerificationCode("alice", "123456", "newPassword"))
|
||||
.thenReturn(true);
|
||||
|
||||
boolean result = forgotPasswordService.forgotPasswordService(body);
|
||||
|
||||
assertThat(result).isTrue();
|
||||
verify(userService).resetPasswordByVerificationCode("alice", "123456", "newPassword");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user