fix(app): 修复 AppController 安全与查询问题
- 加强异常处理、类型安全和图片上传校验 - 优化设备相关查询,避免重复访问数据源 - 补充并记录并发测试与审查修复实施计划
This commit is contained in:
@@ -100,6 +100,12 @@
|
||||
<artifactId>water-common-sse</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@@ -232,4 +232,14 @@ public interface ISysUserService {
|
||||
int updateAppUser(SysUserBo user);
|
||||
|
||||
int updateUserPas(SysUserBo userBo);
|
||||
|
||||
/**
|
||||
* 通过账号标识和验证码重置密码。
|
||||
*
|
||||
* @param username 手机号、邮箱或账号名
|
||||
* @param code 验证码
|
||||
* @param password 新密码明文
|
||||
* @return 是否重置成功
|
||||
*/
|
||||
boolean resetPasswordByVerificationCode(String username, String code, String password);
|
||||
}
|
||||
|
||||
@@ -16,16 +16,17 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.CacheNames;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.dto.UserDTO;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.core.service.UserService;
|
||||
import org.dromara.common.core.utils.*;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.mybatis.helper.DataPermissionHelper;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
@@ -414,6 +415,54 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean resetPasswordByVerificationCode(String username, String code, String password) {
|
||||
if (StringUtils.isBlank(username)) {
|
||||
throw new ServiceException("账号不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(code)) {
|
||||
throw new ServiceException("验证码不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(password)) {
|
||||
throw new ServiceException("新密码不能为空");
|
||||
}
|
||||
|
||||
LambdaQueryWrapper<SysUser> query = Wrappers.lambdaQuery();
|
||||
query.eq(SysUser::getDelFlag, SystemConstants.NORMAL);
|
||||
if (Validator.isEmail(username)) {
|
||||
query.eq(SysUser::getEmail, username);
|
||||
} else if (Validator.isMobile(username)) {
|
||||
query.eq(SysUser::getPhonenumber, username);
|
||||
} else {
|
||||
query.eq(SysUser::getUserName, username);
|
||||
}
|
||||
|
||||
SysUser user = baseMapper.selectOne(query);
|
||||
if (user == null) {
|
||||
throw new UserException("账号未注册");
|
||||
}
|
||||
|
||||
String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
|
||||
String cachedCode = RedisUtils.getCacheObject(cacheKey);
|
||||
if (StringUtils.isBlank(cachedCode)) {
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!StringUtils.equals(cachedCode, code)) {
|
||||
throw new UserException("验证码无效");
|
||||
}
|
||||
|
||||
SysUser update = new SysUser();
|
||||
update.setUserId(user.getUserId());
|
||||
update.setPassword(BCrypt.hashpw(password));
|
||||
int updatedRows = DataPermissionHelper.ignore(() -> baseMapper.updateById(update));
|
||||
if (updatedRows < 1) {
|
||||
throw new ServiceException("忘记密码修改失败");
|
||||
}
|
||||
RedisUtils.deleteObject(cacheKey);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验短信验证码
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package org.dromara.system.service.impl;
|
||||
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
|
||||
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
import org.dromara.common.core.constant.GlobalConstants;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.exception.user.CaptchaExpireException;
|
||||
import org.dromara.common.core.exception.user.UserException;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.system.domain.SysUser;
|
||||
import org.dromara.system.mapper.*;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.redisson.api.RedissonClient;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@Tag("dev")
|
||||
class SysUserServiceImplPasswordRecoveryTest {
|
||||
|
||||
private static GenericApplicationContext applicationContext;
|
||||
|
||||
@Mock private SysUserMapper userMapper;
|
||||
@Mock private SysDeptMapper deptMapper;
|
||||
@Mock private SysRoleMapper roleMapper;
|
||||
@Mock private SysPostMapper postMapper;
|
||||
@Mock private SysUserRoleMapper userRoleMapper;
|
||||
@Mock private SysUserPostMapper userPostMapper;
|
||||
@Captor private ArgumentCaptor<LambdaQueryWrapper<SysUser>> queryCaptor;
|
||||
@Captor private ArgumentCaptor<SysUser> userCaptor;
|
||||
|
||||
@BeforeAll
|
||||
static void initializeInfrastructure() {
|
||||
if (TableInfoHelper.getTableInfo(SysUser.class) == null) {
|
||||
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), ""), SysUser.class);
|
||||
}
|
||||
applicationContext = new GenericApplicationContext();
|
||||
applicationContext.registerBean(RedissonClient.class, () -> mock(RedissonClient.class));
|
||||
applicationContext.refresh();
|
||||
new SpringUtil().setApplicationContext(applicationContext);
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void closeInfrastructure() {
|
||||
applicationContext.close();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"alice@example.com,email",
|
||||
"13305376054,phonenumber",
|
||||
"alice,userName"
|
||||
})
|
||||
void resetPasswordByVerificationCode_supportsAllAccountIdentifiers(
|
||||
String username, String expectedColumn) {
|
||||
SysUserServiceImpl service = newService();
|
||||
SysUser user = user(10L, username);
|
||||
String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
|
||||
when(userMapper.selectOne(any())).thenReturn(user);
|
||||
when(userMapper.updateById(any(SysUser.class))).thenReturn(1);
|
||||
|
||||
try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
|
||||
redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn("123456");
|
||||
redis.when(() -> RedisUtils.deleteObject(cacheKey)).thenReturn(true);
|
||||
|
||||
boolean result = service.resetPasswordByVerificationCode(username, "123456", "newPassword");
|
||||
|
||||
assertThat(result).isTrue();
|
||||
verify(userMapper).selectOne(queryCaptor.capture());
|
||||
assertThat(queryCaptor.getValue().getSqlSegment()).contains(expectedColumn);
|
||||
assertThat(queryCaptor.getValue().getParamNameValuePairs()).containsValue(username);
|
||||
verify(userMapper).updateById(userCaptor.capture());
|
||||
assertThat(userCaptor.getValue().getUserId()).isEqualTo(10L);
|
||||
assertThat(BCrypt.checkpw("newPassword", userCaptor.getValue().getPassword())).isTrue();
|
||||
redis.verify(() -> RedisUtils.deleteObject(cacheKey));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPasswordByVerificationCode_rejectsWrongCodeWithoutUpdatingOrDeleting() {
|
||||
SysUserServiceImpl service = newService();
|
||||
String username = "13305376054";
|
||||
String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
|
||||
when(userMapper.selectOne(any())).thenReturn(user(10L, username));
|
||||
|
||||
try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
|
||||
redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn("123456");
|
||||
|
||||
assertThatThrownBy(() ->
|
||||
service.resetPasswordByVerificationCode(username, "000000", "newPassword"))
|
||||
.isInstanceOf(UserException.class);
|
||||
|
||||
verify(userMapper, never()).updateById(any(SysUser.class));
|
||||
redis.verify(() -> RedisUtils.deleteObject(cacheKey), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPasswordByVerificationCode_rejectsExpiredCodeWithoutUpdating() {
|
||||
SysUserServiceImpl service = newService();
|
||||
String username = "alice@example.com";
|
||||
String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
|
||||
when(userMapper.selectOne(any())).thenReturn(user(10L, username));
|
||||
|
||||
try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
|
||||
redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn(null);
|
||||
|
||||
assertThatThrownBy(() ->
|
||||
service.resetPasswordByVerificationCode(username, "123456", "newPassword"))
|
||||
.isInstanceOf(CaptchaExpireException.class);
|
||||
|
||||
verify(userMapper, never()).updateById(any(SysUser.class));
|
||||
redis.verify(() -> RedisUtils.deleteObject(cacheKey), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPasswordByVerificationCode_keepsCodeWhenDatabaseUpdateFails() {
|
||||
SysUserServiceImpl service = newService();
|
||||
String username = "alice";
|
||||
String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
|
||||
when(userMapper.selectOne(any())).thenReturn(user(10L, username));
|
||||
when(userMapper.updateById(any(SysUser.class))).thenReturn(0);
|
||||
|
||||
try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
|
||||
redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn("123456");
|
||||
|
||||
assertThatThrownBy(() ->
|
||||
service.resetPasswordByVerificationCode(username, "123456", "newPassword"))
|
||||
.isInstanceOf(ServiceException.class);
|
||||
|
||||
redis.verify(() -> RedisUtils.deleteObject(cacheKey), never());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPasswordByVerificationCode_ignoresLoginBasedDataPermissionDuringUpdate() {
|
||||
SysUserServiceImpl service = newService();
|
||||
String username = "alice";
|
||||
String cacheKey = GlobalConstants.CAPTCHA_CODE_KEY + username;
|
||||
when(userMapper.selectOne(any())).thenReturn(user(10L, username));
|
||||
when(userMapper.updateById(any(SysUser.class))).thenAnswer(invocation -> {
|
||||
assertThat(InterceptorIgnoreHelper.willIgnoreDataPermission("SysUserMapper.updateById"))
|
||||
.isTrue();
|
||||
return 1;
|
||||
});
|
||||
|
||||
try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
|
||||
redis.when(() -> RedisUtils.<String>getCacheObject(cacheKey)).thenReturn("123456");
|
||||
redis.when(() -> RedisUtils.deleteObject(cacheKey)).thenReturn(true);
|
||||
|
||||
assertThat(service.resetPasswordByVerificationCode(
|
||||
username, "123456", "newPassword")).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetPasswordByVerificationCode_rejectsUnknownAccountBeforeReadingCode() {
|
||||
SysUserServiceImpl service = newService();
|
||||
when(userMapper.selectOne(any())).thenReturn(null);
|
||||
|
||||
try (MockedStatic<RedisUtils> redis = mockStatic(RedisUtils.class)) {
|
||||
assertThatThrownBy(() ->
|
||||
service.resetPasswordByVerificationCode("missing", "123456", "newPassword"))
|
||||
.isInstanceOf(UserException.class);
|
||||
|
||||
redis.verifyNoInteractions();
|
||||
verify(userMapper, never()).updateById(any(SysUser.class));
|
||||
}
|
||||
}
|
||||
|
||||
private SysUserServiceImpl newService() {
|
||||
return new SysUserServiceImpl(
|
||||
userMapper, deptMapper, roleMapper, postMapper, userRoleMapper, userPostMapper);
|
||||
}
|
||||
|
||||
private SysUser user(Long userId, String username) {
|
||||
SysUser user = new SysUser();
|
||||
user.setUserId(userId);
|
||||
user.setUserName(username);
|
||||
user.setEmail(username);
|
||||
user.setPhonenumber(username);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user