refactor(auth):重构认证模块并增强验证机制

- 重命名并调整 DTO 类结构,将 LoginRequest 和 RegisterRequest 迁移至 request 包- 引入 AuthLoginRequest 和 AuthRegisterRequest 并添加字段验证注解
- 更新 AuthController 使用新的 DTO 并增加 @Valid 参数校验
- 修改 AuthService 接口及实现类,接收 User 实体而非 RegisterRequest
- 添加全局异常处理器 GlobalExceptionHandler 处理参数校验和业务异常
- 新增 ErrorCode 枚举统一管理错误码和消息
- 引入 UserConverter 组件用于 DTO 到实体的转换
- 增强用户注册与登录逻辑,完善异常处理和错误提示
- 移除旧的 BadCredentialsException 捕获逻辑
- 调整 pom.xml 添加 spring-boot-starter-validation 和相关依赖
- 更新 User 实体类,添加完整字段映射和角色关联配置
- 新增 UserInfoService 及其实现,提供用户管理和密码设置功能
-优化 UserMapper 查询方法,支持联表查询用户及其角色信息
- 删除无用的 HLSController 控制器
- 完善 ImageController 文件上传逻辑并更新响应结构
- 添加用户名和密码格式验证工具类 PasswordValidate 和 UsernameValidate
This commit is contained in:
Chaos
2025-11-18 16:44:43 +08:00
parent a22a369afa
commit 8fc7f6554d
33 changed files with 568 additions and 95 deletions

View File

@@ -44,7 +44,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -69,7 +68,6 @@
<scope>provided</scope>
</dependency>
</dependencies>
<build>

View File

@@ -1,9 +1,11 @@
package cn.nopj.chaos_api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "cn.nopj.chaos_api")
@Slf4j
public class ChaosApiWebApplication {
public static void main(String[] args) {
SpringApplication.run(ChaosApiWebApplication.class, args);

View File

@@ -38,10 +38,8 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
log.info("JWT 登录授权过滤器");
String authHeader = request.getHeader(this.tokenHeader);
log.info("authHeader: {}", authHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length()+1);
log.info("authToken={}", authToken);

View File

@@ -1,12 +1,13 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.LoginRequest;
import cn.nopj.chaos_api.dto.RegisterRequest;
import cn.nopj.chaos_api.converter.UserConverter;
import cn.nopj.chaos_api.dto.request.AuthLoginRequest;
import cn.nopj.chaos_api.dto.request.AuthRegisterRequest;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -15,6 +16,11 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 认证控管理
*
*/
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@@ -24,38 +30,34 @@ public class AuthController {
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private UserConverter userConverter;
/**
* 注册
*
* @param registerRequest 注册信息
* @param authRegisterRequest 注册信息
* @return 注册结果
*/
@PostMapping("/register")
public ApiResult<?> register(@RequestBody RegisterRequest registerRequest) {
if (authService.register(registerRequest) != null) {
return ApiResult.success("注册成功");
}
return ApiResult.failed("用户名已存在");
public ApiResult<?> register(@Valid @RequestBody AuthRegisterRequest authRegisterRequest) {
return ApiResult.success(authService.register(userConverter.convert(authRegisterRequest)));
}
/**
* 登录
*
* @param loginRequest 登录信息
* @param authLoginRequest 登录信息
* @return 登录结果
*/
@PostMapping("/login")
public ApiResult<?> login(@RequestBody LoginRequest loginRequest) {
try {
String token = authService.login(loginRequest.getUsername(), loginRequest.getPassword());
public ApiResult<?> login(@RequestBody AuthLoginRequest authLoginRequest) {
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ApiResult.success(tokenMap);
String token = authService.login(authLoginRequest.getUsername(), authLoginRequest.getPassword());
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ApiResult.success(tokenMap);
}catch (BadCredentialsException e){
return ApiResult.failed(e.getMessage());
}
}
}

View File

@@ -1,20 +0,0 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/api/hls")
public class HLSController {
@PreAuthorize("hasAuthority('admin')")
@GetMapping("/")
ApiResult<String> getHLS(){
return ApiResult.success("HLS is radar");
}
}

View File

@@ -1,5 +1,6 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.response.FileUploadResponse;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.ImageService;
import lombok.extern.slf4j.Slf4j;
@@ -9,6 +10,12 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
/**
* 图片管理
*/
@Slf4j
@RestController
@RequestMapping("/api/image")
@@ -16,9 +23,24 @@ public class ImageController {
@Autowired
private ImageService imageService;
/**
* 上传图片
* @param file 文件
* @return 上传结果
*/
@RequestMapping("/upload")
ApiResult<String> uploadImage(@RequestParam("file") MultipartFile file) {
log.info("上传图片");
return ApiResult.success("上传成功");
if (file.isEmpty()){
return ApiResult.failed("上传文件不能为空");
}
try {
String fileName = file.getName();
InputStream inputStream = file.getInputStream();
FileUploadResponse response = imageService.uploadImage(fileName, inputStream);
return ApiResult.success(response.getFileDownloadUri());
}catch (IOException e){
log.error("上传文件失败", e);
return ApiResult.failed("上传文件失败");
}
}
}

View File

@@ -5,6 +5,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;
/**
* 角色管理
*/
@Slf4j
@RestController
public class RoleController {

View File

@@ -1,13 +1,52 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.request.SetUserPasswordRequest;
import cn.nopj.chaos_api.dto.response.UserinfoResponse;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.UserInfoService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 用户管理
*
* @author nopj
*/
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
UserInfoService userInfoService;
/**
* 获取所有用户信息
* @return 所有用户信息
*/
@PreAuthorize("hasAuthority('admin')")
@GetMapping("/all")
ApiResult<List<UserinfoResponse>> getAllUsers(){
return ApiResult.success(userInfoService.getAllUsers());
}
/**
* 设置用户密码
* @param request 设置用户密码请求
* @return 设置用户密码结果
*/
@PreAuthorize("hasAuthority('admin')")
@RequestMapping("/setUserPassword")
ApiResult<String> setUserPassword(@RequestBody SetUserPasswordRequest request){
userInfoService.setUserPassword(request.getUserId(), request.getPassword());
return ApiResult.success("用户密码修改成功");
}
}

View File

@@ -0,0 +1,43 @@
package cn.nopj.chaos_api.exception;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Objects;
@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// 提取第一条错误提示
String errorMsg = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
// 返回统一的错误格式
return ApiResult.failed(errorMsg);
}
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BizException.class)
public ApiResult<?> handleBizException(BizException ex) {
return ApiResult.failed(ex.getErrorCode().getHttpStatus(),ex.getMessage());
}
/**
* 兜底处理:处理所有未知的 RuntimeException
* 防止出现 500 错误页面,给前端返回友好的提示
*/
@ExceptionHandler(Exception.class)
public ApiResult<?> handleAllExceptions(Exception ex) {
log.error("服务器内部错误", ex);
return ApiResult.failed("服务器内部错误,请联系管理员");
}
}

View File

@@ -1,13 +1,12 @@
package cn.nopj.chaos_api.service;
import cn.nopj.chaos_api.domain.entity.User;
import cn.nopj.chaos_api.dto.RegisterRequest;
public interface AuthService {
/**
* 注册
*/
User register(RegisterRequest registerRequest);
User register(User user);
/**
* 登录

View File

@@ -1,13 +1,13 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.User;
import cn.nopj.chaos_api.dto.RegisterRequest;
import cn.nopj.chaos_api.mapper.UserMapper;
import cn.nopj.chaos_api.service.AuthService;
import cn.nopj.chaos_api.util.JwtTokenUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
@@ -18,8 +18,9 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class AuthServiceImpl implements AuthService {
@Autowired
private UserMapper userMapper;
@@ -31,35 +32,45 @@ public class AuthServiceImpl implements AuthService {
private AuthenticationManager authenticationManager;
@Override
public User register(RegisterRequest registerRequest) {
public User register(User user) {
// 检查用户名是否已存在
if (userMapper.selectOne(new QueryWrapper<User>().eq("username", registerRequest.getUsername())) != null) {
if (userMapper.selectByUsername(user.getUsername()) != null) {
// 在实际项目中,这里应该抛出自定义异常
return null;
throw new BizException(ErrorCode.USERNAME_EXISTS);
}
User user = new User();
user.setUsername(registerRequest.getUsername());
// 使用 BCrypt 加密密码
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setPassword(passwordEncoder.encode(user.getPassword()));
userMapper.insert(user);
// 你可以在这里为新用户分配默认角色
userMapper.insertUserRole(user.getId(), 2L);
return user;
}
@Override
public String login(String username, String password) {
// 尝试进行身份验证
try{
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
// 将认证结果保存在 SecurityContextHolder 中
SecurityContextHolder.getContext().setAuthentication(authentication);
// 获取用户详情
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
if (!userDetails.isEnabled()){
if (!userDetails.isEnabled()) {
return null;
}
// 生成 JWT
return jwtTokenUtil.generateToken(userDetails);
}catch (Exception e){
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
}
}

View File

@@ -1,15 +1,16 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.User;
import cn.nopj.chaos_api.mapper.UserMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -22,11 +23,11 @@ public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
public UserDetails loadUserByUsername(String username) {
// 1. 从数据库查询用户
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
// 2. 查询该用户的权限信息 (角色 + 权限)
@@ -38,6 +39,7 @@ public class UserDetailsServiceImpl implements UserDetailsService {
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 4. 构建并返回 Spring Security 的 User 对象
return new org.springframework.security.core.userdetails.User(
user.getUsername(),