|
|
@@ -3,16 +3,24 @@ package com.example.resourceplatform.service.impl;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
import com.example.resourceplatform.common.BusinessException;
|
|
|
+import com.example.resourceplatform.common.UserRole;
|
|
|
import com.example.resourceplatform.dto.LoginDTO;
|
|
|
import com.example.resourceplatform.dto.LoginVO;
|
|
|
import com.example.resourceplatform.dto.PasswordUpdateDTO;
|
|
|
import com.example.resourceplatform.dto.UserDTO;
|
|
|
+import com.example.resourceplatform.dto.UserVO;
|
|
|
+import com.example.resourceplatform.entity.Project;
|
|
|
+import com.example.resourceplatform.entity.Resource;
|
|
|
import com.example.resourceplatform.entity.SysUser;
|
|
|
+import com.example.resourceplatform.mapper.ProjectMapper;
|
|
|
+import com.example.resourceplatform.mapper.ResourceMapper;
|
|
|
import com.example.resourceplatform.mapper.SysUserMapper;
|
|
|
import com.example.resourceplatform.security.JwtUtil;
|
|
|
import com.example.resourceplatform.service.SysUserService;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.security.authentication.AuthenticationManager;
|
|
|
+import org.springframework.security.authentication.BadCredentialsException;
|
|
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
@@ -21,8 +29,10 @@ import org.springframework.util.CollectionUtils;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
+import java.util.concurrent.ConcurrentHashMap;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
+@Slf4j
|
|
|
@Service
|
|
|
@RequiredArgsConstructor
|
|
|
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
|
|
|
@@ -30,12 +40,49 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
|
|
private final AuthenticationManager authenticationManager;
|
|
|
private final JwtUtil jwtUtil;
|
|
|
private final PasswordEncoder passwordEncoder;
|
|
|
+ private final ResourceMapper resourceMapper;
|
|
|
+ private final ProjectMapper projectMapper;
|
|
|
+
|
|
|
+ /** 登录失败计数 key=用户名 */
|
|
|
+ private final ConcurrentHashMap<String, int[]> loginFailCount = new ConcurrentHashMap<>();
|
|
|
+ /** 登录锁定截止时间 key=用户名 */
|
|
|
+ private final ConcurrentHashMap<String, Long> loginLockUntil = new ConcurrentHashMap<>();
|
|
|
+
|
|
|
+ private static final int MAX_FAIL_COUNT = 5;
|
|
|
+ private static final long LOCK_DURATION_MS = 15 * 60 * 1000L; // 15分钟
|
|
|
|
|
|
@Override
|
|
|
public LoginVO login(LoginDTO dto) {
|
|
|
- // 交给Spring Security校验账号密码,失败会抛BadCredentialsException,由全局异常处理器统一返回
|
|
|
- authenticationManager.authenticate(
|
|
|
- new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword()));
|
|
|
+ // 检查是否被锁定
|
|
|
+ Long lockUntil = loginLockUntil.get(dto.getUsername());
|
|
|
+ if (lockUntil != null && System.currentTimeMillis() < lockUntil) {
|
|
|
+ long remainSeconds = (lockUntil - System.currentTimeMillis()) / 1000;
|
|
|
+ throw new BusinessException(403, "账号已被临时锁定,请 " + remainSeconds + " 秒后再试");
|
|
|
+ }
|
|
|
+ // 锁定已过期,清理
|
|
|
+ if (lockUntil != null) {
|
|
|
+ loginLockUntil.remove(dto.getUsername());
|
|
|
+ loginFailCount.remove(dto.getUsername());
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ authenticationManager.authenticate(
|
|
|
+ new UsernamePasswordAuthenticationToken(dto.getUsername(), dto.getPassword()));
|
|
|
+ } catch (BadCredentialsException e) {
|
|
|
+ // 登录失败计数
|
|
|
+ int[] count = loginFailCount.computeIfAbsent(dto.getUsername(), k -> new int[]{0});
|
|
|
+ count[0]++;
|
|
|
+ if (count[0] >= MAX_FAIL_COUNT) {
|
|
|
+ loginLockUntil.put(dto.getUsername(), System.currentTimeMillis() + LOCK_DURATION_MS);
|
|
|
+ loginFailCount.remove(dto.getUsername());
|
|
|
+ throw new BusinessException(403, "连续登录失败 " + MAX_FAIL_COUNT + " 次,账号已被锁定 15 分钟");
|
|
|
+ }
|
|
|
+ throw new BusinessException(401, "账号或密码错误(已失败 " + count[0] + " 次," + MAX_FAIL_COUNT + " 次后锁定)");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 登录成功,清除失败计数
|
|
|
+ loginFailCount.remove(dto.getUsername());
|
|
|
+ loginLockUntil.remove(dto.getUsername());
|
|
|
|
|
|
SysUser user = this.getOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getUsername, dto.getUsername()));
|
|
|
|
|
|
@@ -56,6 +103,7 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
|
|
if (!passwordEncoder.matches(dto.getOldPassword(), user.getPassword())) {
|
|
|
throw new BusinessException("原密码不正确");
|
|
|
}
|
|
|
+ validatePasswordComplexity(dto.getNewPassword());
|
|
|
user.setPassword(passwordEncoder.encode(dto.getNewPassword()));
|
|
|
this.updateById(user);
|
|
|
}
|
|
|
@@ -69,10 +117,11 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
|
|
user.setUsername(dto.getUsername());
|
|
|
user.setRealName(dto.getRealName());
|
|
|
user.setEmail(dto.getEmail());
|
|
|
- user.setRole(dto.getRole() != null ? dto.getRole() : 2);
|
|
|
+ user.setRole(dto.getRole() != null ? dto.getRole() : UserRole.USER.getCode());
|
|
|
user.setStatus(dto.getStatus() != null ? dto.getStatus() : 1);
|
|
|
// 新增必须传密码;编辑时如果传了密码就更新,不传则保留原密码
|
|
|
if (dto.getPassword() != null && !dto.getPassword().isBlank()) {
|
|
|
+ validatePasswordComplexity(dto.getPassword());
|
|
|
user.setPassword(passwordEncoder.encode(dto.getPassword()));
|
|
|
} else if (dto.getId() == null) {
|
|
|
throw new BusinessException("新增用户必须设置初始密码");
|
|
|
@@ -85,12 +134,27 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
|
|
|
|
|
@Override
|
|
|
public void deleteUser(Long id) {
|
|
|
+ // 检查是否为资源或项目负责人
|
|
|
+ long resourceCount = resourceMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<Resource>().eq(Resource::getOwnerId, id));
|
|
|
+ long projectCount = projectMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<Project>().eq(Project::getOwnerId, id));
|
|
|
+ if (resourceCount > 0 || projectCount > 0) {
|
|
|
+ throw new BusinessException("该用户是 " + resourceCount + " 个资源和 " + projectCount
|
|
|
+ + " 个项目的负责人,请先转移负责人后再删除");
|
|
|
+ }
|
|
|
this.removeById(id);
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- public List<SysUser> listAllEnabled() {
|
|
|
- return this.list(new LambdaQueryWrapper<SysUser>().eq(SysUser::getStatus, 1));
|
|
|
+ public List<UserVO> listAllVO() {
|
|
|
+ return this.list().stream().map(this::toVO).toList();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public List<UserVO> listAllEnabledVO() {
|
|
|
+ return this.list(new LambdaQueryWrapper<SysUser>().eq(SysUser::getStatus, 1))
|
|
|
+ .stream().map(this::toVO).toList();
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
@@ -101,4 +165,28 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
|
|
|
return this.listByIds(userIds).stream()
|
|
|
.collect(Collectors.toMap(SysUser::getId, SysUser::getRealName));
|
|
|
}
|
|
|
+
|
|
|
+ private UserVO toVO(SysUser user) {
|
|
|
+ UserVO vo = new UserVO();
|
|
|
+ vo.setId(user.getId());
|
|
|
+ vo.setUsername(user.getUsername());
|
|
|
+ vo.setRealName(user.getRealName());
|
|
|
+ vo.setEmail(user.getEmail());
|
|
|
+ vo.setRole(user.getRole());
|
|
|
+ vo.setStatus(user.getStatus());
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 密码复杂度校验:至少6位,必须同时包含字母和数字 */
|
|
|
+ private void validatePasswordComplexity(String password) {
|
|
|
+ if (password == null || password.length() < 6) {
|
|
|
+ throw new BusinessException("密码长度不能少于6位");
|
|
|
+ }
|
|
|
+ boolean hasLetter = password.chars().anyMatch(Character::isLetter);
|
|
|
+ boolean hasDigit = password.chars().anyMatch(Character::isDigit);
|
|
|
+ if (!hasLetter || !hasDigit) {
|
|
|
+ throw new BusinessException("密码必须同时包含字母和数字");
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
+
|