refactor(auth): 统一异常处理和API响应规范

- 添加统一响应类 ApiResponse
- 添加错误码枚举 ErrorCode
- 添加业务异常类 BusinessException
- 添加全局异常处理器 GlobalExceptionHandler
- 登录接口改为POST body
- 统一登录错误信息,避免用户枚举
- 更新开发规范文档
This commit is contained in:
chiguyong 2026-03-17 22:48:44 +08:00
parent 2f8ac15434
commit 53381e2670
7 changed files with 220 additions and 49 deletions

View File

@ -2,7 +2,9 @@ package com.ether.pms.auth.controller;
import com.ether.pms.auth.service.LoginService; import com.ether.pms.auth.service.LoginService;
import com.ether.pms.auth.util.JwtTokenProvider; import com.ether.pms.auth.util.JwtTokenProvider;
import com.ether.pms.common.ApiResponse;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -19,56 +21,55 @@ public class AuthController {
private final JwtTokenProvider jwtTokenProvider; private final JwtTokenProvider jwtTokenProvider;
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<Map<String, Object>> login( public ResponseEntity<ApiResponse<Map<String, Object>>> login(
@RequestParam String username, @RequestBody LoginRequest request,
@RequestParam String password, HttpServletRequest httpRequest) {
HttpServletRequest request) {
String ip = getClientIp(request); String ip = getClientIp(httpRequest);
Map<String, Object> result = loginService.login(username, password, ip); Map<String, Object> result = loginService.login(request.getUsername(), request.getPassword(), ip);
return ResponseEntity.ok(result); return ResponseEntity.ok(ApiResponse.success(result));
} }
@PostMapping("/logout") @PostMapping("/logout")
public ResponseEntity<Void> logout(@RequestHeader(value = "Authorization", required = false) String token) { public ResponseEntity<ApiResponse<Void>> logout(@RequestHeader(value = "Authorization", required = false) String token) {
return ResponseEntity.ok().build(); return ResponseEntity.ok(ApiResponse.success());
} }
@GetMapping("/me") @GetMapping("/me")
public ResponseEntity<Map<String, Object>> getCurrentUser( public ResponseEntity<ApiResponse<Map<String, Object>>> getCurrentUser(
@RequestHeader(value = "Authorization", required = false) String token) { @RequestHeader(value = "Authorization", required = false) String token) {
if (token == null || !token.startsWith("Bearer ")) { if (token == null || !token.startsWith("Bearer ")) {
return ResponseEntity.status(401).build(); return ResponseEntity.ok(ApiResponse.error(401, "未授权"));
} }
String jwt = token.substring(7); String jwt = token.substring(7);
if (!jwtTokenProvider.validateToken(jwt)) { if (!jwtTokenProvider.validateToken(jwt)) {
return ResponseEntity.status(401).build(); return ResponseEntity.ok(ApiResponse.error(401, "Token无效"));
} }
String username = jwtTokenProvider.getUsernameFromToken(jwt); String username = jwtTokenProvider.getUsernameFromToken(jwt);
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("username", username); result.put("username", username);
return ResponseEntity.ok(result); return ResponseEntity.ok(ApiResponse.success(result));
} }
@PostMapping("/refresh") @PostMapping("/refresh")
public ResponseEntity<Map<String, Object>> refreshToken( public ResponseEntity<ApiResponse<Map<String, Object>>> refreshToken(
@RequestHeader(value = "Authorization", required = false) String token) { @RequestHeader(value = "Authorization", required = false) String token) {
if (token == null || !token.startsWith("Bearer ")) { if (token == null || !token.startsWith("Bearer ")) {
return ResponseEntity.status(401).build(); return ResponseEntity.ok(ApiResponse.error(401, "未授权"));
} }
String jwt = token.substring(7); String jwt = token.substring(7);
if (!jwtTokenProvider.validateToken(jwt)) { if (!jwtTokenProvider.validateToken(jwt)) {
return ResponseEntity.status(401).build(); return ResponseEntity.ok(ApiResponse.error(401, "Token无效"));
} }
if (jwtTokenProvider.isTokenExpired(jwt)) { if (jwtTokenProvider.isTokenExpired(jwt)) {
return ResponseEntity.status(401).build(); return ResponseEntity.ok(ApiResponse.error(401, "Token已过期"));
} }
String username = jwtTokenProvider.getUsernameFromToken(jwt); String username = jwtTokenProvider.getUsernameFromToken(jwt);
@ -78,7 +79,7 @@ public class AuthController {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
result.put("token", newToken); result.put("token", newToken);
return ResponseEntity.ok(result); return ResponseEntity.ok(ApiResponse.success(result));
} }
private String getClientIp(HttpServletRequest request) { private String getClientIp(HttpServletRequest request) {
@ -91,4 +92,10 @@ public class AuthController {
} }
return ip; return ip;
} }
@Data
public static class LoginRequest {
private String username;
private String password;
}
} }

View File

@ -3,6 +3,8 @@ package com.ether.pms.auth.service;
import com.ether.pms.auth.entity.User; import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.repository.UserRepository; import com.ether.pms.auth.repository.UserRepository;
import com.ether.pms.auth.util.JwtTokenProvider; import com.ether.pms.auth.util.JwtTokenProvider;
import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -18,34 +20,25 @@ public class LoginService {
private final PasswordService passwordService; private final PasswordService passwordService;
private final LoginAttemptService loginAttemptService; private final LoginAttemptService loginAttemptService;
private static final int MAX_ATTEMPTS = 5;
public Map<String, Object> login(String username, String password, String ip) { public Map<String, Object> login(String username, String password, String ip) {
if (loginAttemptService.isLockedOut(username)) { if (loginAttemptService.isLockedOut(username)) {
int remaining = loginAttemptService.getRemainingAttempts(username); throw new BusinessException(ErrorCode.AUTH_002);
throw new RuntimeException("账号已被锁定,请" + 10 + "分钟后再试");
} }
User user = userRepository.findByUsername(username) User user = userRepository.findByUsername(username).orElse(null);
.orElseThrow(() -> {
if (user == null || !passwordService.matches(password, user.getPassword())) {
if (user != null) {
loginAttemptService.recordFailure(username); loginAttemptService.recordFailure(username);
return new RuntimeException("用户名或密码错误"); }
}); throw new BusinessException(ErrorCode.AUTH_001);
}
if (user.getStatus() == User.UserStatus.LOCKED) { if (user.getStatus() == User.UserStatus.LOCKED) {
throw new RuntimeException("账号已被锁定"); throw new BusinessException(ErrorCode.AUTH_002);
} }
if (user.getStatus() == User.UserStatus.DISABLED) { if (user.getStatus() == User.UserStatus.DISABLED) {
throw new RuntimeException("账号已被禁用"); throw new BusinessException(ErrorCode.AUTH_003);
}
if (!passwordService.matches(password, user.getPassword())) {
loginAttemptService.recordFailure(username);
int remaining = loginAttemptService.getRemainingAttempts(username);
if (remaining <= 0) {
throw new RuntimeException("账号已被锁定请10分钟后再试");
}
throw new RuntimeException("用户名或密码错误,剩余尝试次数: " + remaining);
} }
loginAttemptService.recordSuccess(username); loginAttemptService.recordSuccess(username);

View File

@ -4,6 +4,8 @@ import com.ether.pms.auth.entity.User;
import com.ether.pms.auth.entity.Role; import com.ether.pms.auth.entity.Role;
import com.ether.pms.auth.repository.UserRepository; import com.ether.pms.auth.repository.UserRepository;
import com.ether.pms.auth.repository.RoleRepository; import com.ether.pms.auth.repository.RoleRepository;
import com.ether.pms.common.BusinessException;
import com.ether.pms.common.ErrorCode;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@ -26,27 +28,31 @@ public class UserService {
public User findById(UUID id) { public User findById(UUID id) {
return userRepository.findById(id) return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException("用户不存在")); .orElseThrow(() -> new BusinessException(ErrorCode.USER_003));
} }
public User findByUsername(String username) { public User findByUsername(String username) {
return userRepository.findByUsername(username) return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("用户不存在")); .orElseThrow(() -> new BusinessException(ErrorCode.USER_003));
} }
@Transactional @Transactional
public User create(User user) { public User create(User user) {
if (userRepository.existsByUsername(user.getUsername())) { if (userRepository.existsByUsername(user.getUsername())) {
throw new RuntimeException("用户名已存在"); throw new BusinessException(ErrorCode.USER_001);
} }
if (user.getPhone() != null && userRepository.existsByPhone(user.getPhone())) { if (user.getPhone() != null && userRepository.existsByPhone(user.getPhone())) {
throw new RuntimeException("手机号已存在"); throw new BusinessException(ErrorCode.USER_002);
} }
passwordService.validatePassword(user.getPassword()); try {
passwordService.validatePassword(user.getPassword());
} catch (IllegalArgumentException e) {
throw new BusinessException(ErrorCode.BAD_REQUEST, e.getMessage());
}
if (passwordService.isPasswordWeak(user.getPassword())) { if (passwordService.isPasswordWeak(user.getPassword())) {
throw new RuntimeException("密码太弱,请使用更复杂的密码"); throw new BusinessException(ErrorCode.BAD_REQUEST, "密码太弱,请使用更复杂的密码");
} }
user.setPassword(passwordService.encode(user.getPassword())); user.setPassword(passwordService.encode(user.getPassword()));
@ -63,7 +69,7 @@ public class UserService {
} }
if (user.getPhone() != null) { if (user.getPhone() != null) {
if (!user.getPhone().equals(existing.getPhone()) && userRepository.existsByPhone(user.getPhone())) { if (!user.getPhone().equals(existing.getPhone()) && userRepository.existsByPhone(user.getPhone())) {
throw new RuntimeException("手机号已存在"); throw new BusinessException(ErrorCode.USER_002);
} }
existing.setPhone(user.getPhone()); existing.setPhone(user.getPhone());
} }
@ -85,13 +91,17 @@ public class UserService {
User user = findById(id); User user = findById(id);
if (!passwordService.matches(oldPassword, user.getPassword())) { if (!passwordService.matches(oldPassword, user.getPassword())) {
throw new RuntimeException("原密码错误"); throw new BusinessException(ErrorCode.USER_004);
} }
passwordService.validatePassword(newPassword); try {
passwordService.validatePassword(newPassword);
} catch (IllegalArgumentException e) {
throw new BusinessException(ErrorCode.BAD_REQUEST, e.getMessage());
}
if (passwordService.isPasswordWeak(newPassword)) { if (passwordService.isPasswordWeak(newPassword)) {
throw new RuntimeException("新密码太弱,请使用更复杂的密码"); throw new BusinessException(ErrorCode.BAD_REQUEST, "新密码太弱,请使用更复杂的密码");
} }
user.setPassword(passwordService.encode(newPassword)); user.setPassword(passwordService.encode(newPassword));
@ -102,10 +112,14 @@ public class UserService {
public void resetPassword(UUID id, String newPassword) { public void resetPassword(UUID id, String newPassword) {
User user = findById(id); User user = findById(id);
passwordService.validatePassword(newPassword); try {
passwordService.validatePassword(newPassword);
} catch (IllegalArgumentException e) {
throw new BusinessException(ErrorCode.BAD_REQUEST, e.getMessage());
}
if (passwordService.isPasswordWeak(newPassword)) { if (passwordService.isPasswordWeak(newPassword)) {
throw new RuntimeException("密码太弱,请使用更复杂的密码"); throw new BusinessException(ErrorCode.BAD_REQUEST, "密码太弱,请使用更复杂的密码");
} }
user.setPassword(passwordService.encode(newPassword)); user.setPassword(passwordService.encode(newPassword));

View File

@ -0,0 +1,45 @@
package com.ether.pms.common;
import lombok.Data;
@Data
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public ApiResponse() {
}
public ApiResponse(int code, String message) {
this.code = code;
this.message = message;
}
public ApiResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public static <T> ApiResponse<T> success() {
return new ApiResponse<>(200, "success");
}
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data);
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<>(200, message, data);
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(500, message);
}
}

View File

@ -0,0 +1,28 @@
package com.ether.pms.common;
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final int code;
private final String message;
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
public BusinessException(ErrorCode errorCode, String customMessage) {
super(customMessage);
this.code = errorCode.getCode();
this.message = customMessage;
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
this.message = message;
}
}

View File

@ -0,0 +1,48 @@
package com.ether.pms.common;
public enum ErrorCode {
SUCCESS(200, "success"),
BAD_REQUEST(400, "请求参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
AUTH_001(1001, "用户名或密码错误"),
AUTH_002(1002, "账号已被锁定"),
AUTH_003(1003, "账号已被禁用"),
AUTH_004(1004, "Token已过期"),
AUTH_005(1005, "Token无效"),
USER_001(2001, "用户名已存在"),
USER_002(2002, "手机号已存在"),
USER_003(2003, "用户不存在"),
USER_004(2004, "原密码错误"),
ROLE_001(3001, "角色编码已存在"),
ROLE_002(3002, "角色不存在"),
PERMISSION_001(4001, "权限编码已存在"),
PROJECT_001(5001, "项目编码已存在"),
PROJECT_002(5002, "项目不存在"),
SYSTEM_ERROR(9999, "系统错误");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}

View File

@ -0,0 +1,36 @@
package com.ether.pms.common;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException e) {
log.warn("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.error(e.getCode(), e.getMessage()));
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<ApiResponse<Void>> handleIllegalArgumentException(IllegalArgumentException e) {
log.warn("参数异常: {}", e.getMessage());
return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.error(400, e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.error(ErrorCode.SYSTEM_ERROR.getCode(), "系统错误,请稍后重试"));
}
}