错误码梳理

This commit is contained in:
waner
2026-04-17 16:59:18 +08:00
parent a805480d01
commit a02c9959b7
17 changed files with 250 additions and 147 deletions
@@ -3,9 +3,13 @@ package com.cisd.tms.common.enums;
public enum ErrorCode { public enum ErrorCode {
BAD_REQUEST(400, "bad request"), BAD_REQUEST(400, "bad request"),
UNAUTHORIZED(4010, "unauthorized"), UNAUTHORIZED(4010, "unauthorized"),
SESSION_INVALID(4011, "session invalid"),
FORBIDDEN(4030, "forbidden"),
VALIDATE_FAILED(4001, "parameter validation failed"), VALIDATE_FAILED(4001, "parameter validation failed"),
CONFLICT(4090, "conflict"),
BIZ_ERROR(5001, "business exception"), BIZ_ERROR(5001, "business exception"),
CRYPTO_CARD_ERROR(5201, "crypto card adapter error"), CRYPTO_CARD_ERROR(5201, "crypto card adapter error"),
SERVICE_UNAVAILABLE(5030, "service unavailable"),
INTERNAL_ERROR(5000, "internal server error"); INTERNAL_ERROR(5000, "internal server error");
private final int code; private final int code;
@@ -8,6 +8,8 @@ import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -20,7 +22,7 @@ public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class); private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(MethodArgumentNotValidException.class) @ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Void> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleMethodArgumentNotValid(MethodArgumentNotValidException ex, HttpServletRequest request) {
String msg = ex.getBindingResult().getAllErrors().stream() String msg = ex.getBindingResult().getAllErrors().stream()
.findFirst() .findFirst()
.map(error -> { .map(error -> {
@@ -30,53 +32,84 @@ public class GlobalExceptionHandler {
return error.getDefaultMessage(); return error.getDefaultMessage();
}) })
.orElse(ErrorCode.VALIDATE_FAILED.getMessage()); .orElse(ErrorCode.VALIDATE_FAILED.getMessage());
return ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), msg).withPath(request.getRequestURI()); return response(HttpStatus.BAD_REQUEST, ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), msg).withPath(request.getRequestURI()));
} }
@ExceptionHandler(ConstraintViolationException.class) @ExceptionHandler(ConstraintViolationException.class)
public ApiResponse<Void> handleConstraintViolation(ConstraintViolationException ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleConstraintViolation(ConstraintViolationException ex, HttpServletRequest request) {
return ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), ex.getMessage()).withPath(request.getRequestURI()); return response(HttpStatus.BAD_REQUEST,
ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), ex.getMessage()).withPath(request.getRequestURI()));
} }
@ExceptionHandler(BizException.class) @ExceptionHandler(BizException.class)
public ApiResponse<Void> handleBizException(BizException ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleBizException(BizException ex, HttpServletRequest request) {
return ApiResponse.fail(ex.getCode(), ex.getMessage()).withPath(request.getRequestURI()); return response(resolveHttpStatus(ex.getCode()),
ApiResponse.fail(ex.getCode(), ex.getMessage()).withPath(request.getRequestURI()));
} }
@ExceptionHandler(IllegalArgumentException.class) @ExceptionHandler(IllegalArgumentException.class)
public ApiResponse<Void> handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleIllegalArgument(IllegalArgumentException ex, HttpServletRequest request) {
return ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), ex.getMessage()).withPath(request.getRequestURI()); return response(HttpStatus.BAD_REQUEST,
ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), ex.getMessage()).withPath(request.getRequestURI()));
} }
@ExceptionHandler(MaxUploadSizeExceededException.class) @ExceptionHandler(MaxUploadSizeExceededException.class)
public ApiResponse<Void> handleMaxUploadSizeExceeded(MaxUploadSizeExceededException ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleMaxUploadSizeExceeded(MaxUploadSizeExceededException ex, HttpServletRequest request) {
return ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), "upload file size exceeds limit") return response(HttpStatus.BAD_REQUEST, ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), "upload file size exceeds limit")
.withPath(request.getRequestURI()); .withPath(request.getRequestURI()));
} }
@ExceptionHandler(MultipartException.class) @ExceptionHandler(MultipartException.class)
public ApiResponse<Void> handleMultipartException(MultipartException ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleMultipartException(MultipartException ex, HttpServletRequest request) {
return ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), "failed to parse multipart request") return response(HttpStatus.BAD_REQUEST, ApiResponse.fail(ErrorCode.VALIDATE_FAILED.getCode(), "failed to parse multipart request")
.withPath(request.getRequestURI()); .withPath(request.getRequestURI()));
} }
@ExceptionHandler(PcieCryptoException.class) @ExceptionHandler(PcieCryptoException.class)
public ApiResponse<Void> handleCryptoCardException(PcieCryptoException ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleCryptoCardException(PcieCryptoException ex, HttpServletRequest request) {
return ApiResponse.fail(ErrorCode.CRYPTO_CARD_ERROR.getCode(), ex.getMessage()) return response(HttpStatus.INTERNAL_SERVER_ERROR, ApiResponse.fail(ErrorCode.CRYPTO_CARD_ERROR.getCode(), ex.getMessage())
.withPath(request.getRequestURI()); .withPath(request.getRequestURI()));
} }
@ExceptionHandler(ReplayProtectionException.class) @ExceptionHandler(ReplayProtectionException.class)
public ApiResponse<Void> handleReplayProtectionException(ReplayProtectionException ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleReplayProtectionException(ReplayProtectionException ex, HttpServletRequest request) {
log.warn("Replay protection failed", ex); log.warn("Replay protection failed", ex);
return ApiResponse.fail(ErrorCode.INTERNAL_ERROR.getCode(), "replay protection unavailable") return response(HttpStatus.SERVICE_UNAVAILABLE,
.withPath(request.getRequestURI()); ApiResponse.fail(ErrorCode.SERVICE_UNAVAILABLE.getCode(), "replay protection unavailable")
.withPath(request.getRequestURI()));
} }
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception ex, HttpServletRequest request) { public ResponseEntity<ApiResponse<Void>> handleException(Exception ex, HttpServletRequest request) {
log.error("Unhandled exception", ex); log.error("Unhandled exception", ex);
return ApiResponse.fail(ErrorCode.INTERNAL_ERROR.getCode(), ErrorCode.INTERNAL_ERROR.getMessage()) return response(HttpStatus.INTERNAL_SERVER_ERROR,
.withPath(request.getRequestURI()); ApiResponse.fail(ErrorCode.INTERNAL_ERROR.getCode(), ErrorCode.INTERNAL_ERROR.getMessage())
.withPath(request.getRequestURI()));
}
private ResponseEntity<ApiResponse<Void>> response(HttpStatus status, ApiResponse<Void> body) {
return ResponseEntity.status(status).body(body);
}
private HttpStatus resolveHttpStatus(int code) {
if (code == ErrorCode.BAD_REQUEST.getCode() || code == ErrorCode.VALIDATE_FAILED.getCode()) {
return HttpStatus.BAD_REQUEST;
}
if (code == ErrorCode.UNAUTHORIZED.getCode()) {
return HttpStatus.UNAUTHORIZED;
}
if (code == ErrorCode.SESSION_INVALID.getCode()) {
return HttpStatus.UNAUTHORIZED;
}
if (code == ErrorCode.FORBIDDEN.getCode()) {
return HttpStatus.FORBIDDEN;
}
if (code == ErrorCode.CONFLICT.getCode()) {
return HttpStatus.CONFLICT;
}
if (code == ErrorCode.SERVICE_UNAVAILABLE.getCode()) {
return HttpStatus.SERVICE_UNAVAILABLE;
}
return HttpStatus.INTERNAL_SERVER_ERROR;
} }
} }
@@ -63,7 +63,7 @@ public class InternalAuthorizationInterceptor implements HandlerInterceptor {
HttpResponseUtil.writeJson( HttpResponseUtil.writeJson(
response, response,
HttpServletResponse.SC_FORBIDDEN, HttpServletResponse.SC_FORBIDDEN,
ApiResponse.fail(ErrorCode.UNAUTHORIZED.getCode(), message), ApiResponse.fail(ErrorCode.FORBIDDEN.getCode(), message),
objectMapper objectMapper
); );
} }
@@ -461,9 +461,9 @@ public class AuthServiceImpl implements AuthService {
private AuthSessionEntity requireActiveSession(String sessionToken) { private AuthSessionEntity requireActiveSession(String sessionToken) {
AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken) AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session not found")); .orElseThrow(() -> new BizException(ErrorCode.SESSION_INVALID.getCode(), "session not found"));
if (session.getLogoutAt() != null || session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) { if (session.getLogoutAt() != null || session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "session expired"); throw new BizException(ErrorCode.SESSION_INVALID.getCode(), "session expired");
} }
return session; return session;
} }
@@ -2,6 +2,8 @@ package com.cisd.tms.modules.device.controller;
import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.device.dto.network.*; import com.cisd.tms.modules.device.dto.network.*;
import com.cisd.tms.modules.device.service.NetworkConfigService; import com.cisd.tms.modules.device.service.NetworkConfigService;
import com.cisd.tms.modules.log.annotation.AuditedOperation; import com.cisd.tms.modules.log.annotation.AuditedOperation;
@@ -122,7 +124,7 @@ public class NetworkConfigController {
String mode = networkConfigService.getBondMode(bondName); String mode = networkConfigService.getBondMode(bondName);
if (mode == null) { if (mode == null) {
return ApiResponse.fail(400, "未找到该连接的 Bond 模式", null); throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "未找到该连接的 Bond 模式");
} }
return ApiResponse.success(mode); return ApiResponse.success(mode);
@@ -1,6 +1,10 @@
package com.cisd.tms.modules.device.filter; package com.cisd.tms.modules.device.filter;
import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.util.HttpResponseUtil;
import com.cisd.tms.modules.device.service.IpWhitelistService; import com.cisd.tms.modules.device.service.IpWhitelistService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -17,6 +21,9 @@ public class IpWhitelistFilter implements Filter {
@Autowired @Autowired
private IpWhitelistService ipWhitelistService; private IpWhitelistService ipWhitelistService;
@Autowired
private ObjectMapper objectMapper;
@Override @Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
@@ -39,9 +46,7 @@ public class IpWhitelistFilter implements Filter {
try { try {
clientIpInt = ipToInt(clientIp); clientIpInt = ipToInt(clientIp);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); writeForbidden(response, "非法的客户端 IP 格式,拒绝访问");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\": 403, \"msg\": \"非法的客户端 IP 格式,拒绝访问\"}");
return; return;
} }
@@ -72,9 +77,7 @@ public class IpWhitelistFilter implements Filter {
// 默认拒绝 // 默认拒绝
if (!isAllowed) { if (!isAllowed) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN); writeForbidden(response, "非法访问:您的IP不在允许的白名单内");
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\"code\": 403, \"msg\": \"非法访问:您的IP不在允许的白名单内\"}");
return; return;
} }
@@ -136,4 +139,13 @@ public class IpWhitelistFilter implements Filter {
return result; return result;
} }
private void writeForbidden(HttpServletResponse response, String message) throws IOException {
HttpResponseUtil.writeJson(
response,
HttpServletResponse.SC_FORBIDDEN,
ApiResponse.fail(ErrorCode.FORBIDDEN.getCode(), message),
objectMapper
);
}
} }
@@ -2,6 +2,8 @@ package com.cisd.tms.modules.log.controller;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.cisd.tms.common.api.ApiResponse; import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.log.dto.AuditLogReviewRequest; import com.cisd.tms.modules.log.dto.AuditLogReviewRequest;
import com.cisd.tms.modules.log.dto.OperationAuditLogPageRequest; import com.cisd.tms.modules.log.dto.OperationAuditLogPageRequest;
import com.cisd.tms.modules.log.dto.OperationAuditLogPageResponse; import com.cisd.tms.modules.log.dto.OperationAuditLogPageResponse;
@@ -50,7 +52,7 @@ public class OperationAuditController {
// 权限校验:仅审计管理员可操作 // 权限校验:仅审计管理员可操作
if (roleObj == null || !roleObj.toString().equals("AUDIT_ADMIN")) { if (roleObj == null || !roleObj.toString().equals("AUDIT_ADMIN")) {
return ApiResponse.fail(403, "权限不足:仅审计管理员可执行此操作"); throw new BizException(ErrorCode.FORBIDDEN.getCode(), "权限不足:仅审计管理员可执行此操作");
} }
auditorUser = roleObj.toString(); auditorUser = roleObj.toString();
@@ -51,17 +51,17 @@ public class InternalApiAuthInterceptor implements HandlerInterceptor {
String sessionToken = extractSessionToken(request); String sessionToken = extractSessionToken(request);
if (sessionToken == null || sessionToken.isBlank()) { if (sessionToken == null || sessionToken.isBlank()) {
writeUnauthorized(response, "session token missing"); writeSessionInvalid(response, "session token missing");
return false; return false;
} }
AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken).orElse(null); AuthSessionEntity session = authSessionRepository.findBySessionToken(sessionToken).orElse(null);
if (session == null || session.getLogoutAt() != null) { if (session == null || session.getLogoutAt() != null) {
writeUnauthorized(response, "session not found"); writeSessionInvalid(response, "session not found");
return false; return false;
} }
if (session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) { if (session.getExpiresAt() == null || !session.getExpiresAt().isAfter(now())) {
writeUnauthorized(response, "session expired"); writeSessionInvalid(response, "session expired");
return false; return false;
} }
@@ -99,4 +99,13 @@ public class InternalApiAuthInterceptor implements HandlerInterceptor {
objectMapper objectMapper
); );
} }
private void writeSessionInvalid(HttpServletResponse response, String message) throws IOException {
HttpResponseUtil.writeJson(
response,
HttpServletResponse.SC_UNAUTHORIZED,
ApiResponse.fail(ErrorCode.SESSION_INVALID.getCode(), message),
objectMapper
);
}
} }
@@ -69,11 +69,11 @@ public class InternalApiReplayInterceptor implements HandlerInterceptor {
try { try {
ReplayCheckResult result = replayProtectionService.check(replayRequest); ReplayCheckResult result = replayProtectionService.check(replayRequest);
if (result.isReplayed()) { if (result.isReplayed()) {
writeJson(response, HttpServletResponse.SC_CONFLICT, ErrorCode.UNAUTHORIZED.getCode(), "replayed nonce"); writeJson(response, HttpServletResponse.SC_CONFLICT, ErrorCode.CONFLICT.getCode(), "replayed nonce");
return false; return false;
} }
} catch (ReplayProtectionException ex) { } catch (ReplayProtectionException ex) {
writeJson(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE, ErrorCode.INTERNAL_ERROR.getCode(), writeJson(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE, ErrorCode.SERVICE_UNAVAILABLE.getCode(),
"replay protection unavailable"); "replay protection unavailable");
return false; return false;
} }
@@ -89,11 +89,12 @@ public class OpenApiSignAuthInterceptor implements HandlerInterceptor {
// 先完成签名校验,再占用 nonce,避免伪造请求提前消耗合法 nonce。 // 先完成签名校验,再占用 nonce,避免伪造请求提前消耗合法 nonce。
ReplayCheckResult result = replayProtectionService.check(buildReplayRequest(request, appId, nonce, requestEpoch)); ReplayCheckResult result = replayProtectionService.check(buildReplayRequest(request, appId, nonce, requestEpoch));
if (result.isReplayed()) { if (result.isReplayed()) {
writeUnauthorized(response, "replayed nonce"); writeJson(response, HttpServletResponse.SC_CONFLICT, ErrorCode.CONFLICT.getCode(), "replayed nonce");
return false; return false;
} }
} catch (ReplayProtectionException ex) { } catch (ReplayProtectionException ex) {
writeUnauthorized(response, "replay protection unavailable"); writeJson(response, HttpServletResponse.SC_SERVICE_UNAVAILABLE,
ErrorCode.SERVICE_UNAVAILABLE.getCode(), "replay protection unavailable");
return false; return false;
} }
@@ -136,10 +137,14 @@ public class OpenApiSignAuthInterceptor implements HandlerInterceptor {
} }
private void writeUnauthorized(HttpServletResponse response, String message) throws IOException { private void writeUnauthorized(HttpServletResponse response, String message) throws IOException {
writeJson(response, HttpServletResponse.SC_UNAUTHORIZED, ErrorCode.UNAUTHORIZED.getCode(), message);
}
private void writeJson(HttpServletResponse response, int status, int code, String message) throws IOException {
HttpResponseUtil.writeJson( HttpResponseUtil.writeJson(
response, response,
HttpServletResponse.SC_UNAUTHORIZED, status,
ApiResponse.fail(ErrorCode.UNAUTHORIZED.getCode(), message), ApiResponse.fail(code, message),
objectMapper objectMapper
); );
} }
@@ -4,6 +4,8 @@ import com.cisd.tms.common.api.ApiResponse;
import com.cisd.tms.common.enums.ErrorCode; import com.cisd.tms.common.enums.ErrorCode;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.multipart.MaxUploadSizeExceededException; import org.springframework.web.multipart.MaxUploadSizeExceededException;
import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartException;
@@ -15,15 +17,17 @@ class GlobalExceptionHandlerTest {
GlobalExceptionHandler handler = new GlobalExceptionHandler(); GlobalExceptionHandler handler = new GlobalExceptionHandler();
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/upgrade-packages"); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/upgrade-packages");
ApiResponse<Void> response = handler.handleMaxUploadSizeExceeded( ResponseEntity<ApiResponse<Void>> response = handler.handleMaxUploadSizeExceeded(
new MaxUploadSizeExceededException(104857600), new MaxUploadSizeExceededException(104857600),
request request
); );
Assertions.assertFalse(response.isSuccess()); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
Assertions.assertEquals(ErrorCode.VALIDATE_FAILED.getCode(), response.getCode()); Assertions.assertNotNull(response.getBody());
Assertions.assertEquals("upload file size exceeds limit", response.getMsg()); Assertions.assertFalse(response.getBody().isSuccess());
Assertions.assertEquals("/api/v1/upgrade-packages", response.getPath()); Assertions.assertEquals(ErrorCode.VALIDATE_FAILED.getCode(), response.getBody().getCode());
Assertions.assertEquals("upload file size exceeds limit", response.getBody().getMsg());
Assertions.assertEquals("/api/v1/upgrade-packages", response.getBody().getPath());
} }
@Test @Test
@@ -31,14 +35,16 @@ class GlobalExceptionHandlerTest {
GlobalExceptionHandler handler = new GlobalExceptionHandler(); GlobalExceptionHandler handler = new GlobalExceptionHandler();
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/files/upload"); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/files/upload");
ApiResponse<Void> response = handler.handleMultipartException( ResponseEntity<ApiResponse<Void>> response = handler.handleMultipartException(
new MultipartException("Failed to parse multipart servlet request"), new MultipartException("Failed to parse multipart servlet request"),
request request
); );
Assertions.assertFalse(response.isSuccess()); Assertions.assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
Assertions.assertEquals(ErrorCode.VALIDATE_FAILED.getCode(), response.getCode()); Assertions.assertNotNull(response.getBody());
Assertions.assertEquals("failed to parse multipart request", response.getMsg()); Assertions.assertFalse(response.getBody().isSuccess());
Assertions.assertEquals("/api/v1/files/upload", response.getPath()); Assertions.assertEquals(ErrorCode.VALIDATE_FAILED.getCode(), response.getBody().getCode());
Assertions.assertEquals("failed to parse multipart request", response.getBody().getMsg());
Assertions.assertEquals("/api/v1/files/upload", response.getBody().getPath());
} }
} }
@@ -1,86 +1,79 @@
//package com.cisd.tms.modules.auth.security; package com.cisd.tms.modules.auth.security;
//
//import com.cisd.tms.modules.log.service.OperationAuditService; import com.cisd.tms.common.enums.ErrorCode;
//import com.cisd.tms.security.internal.InternalApiAuthInterceptor; import com.cisd.tms.modules.log.service.OperationAuditService;
//import com.fasterxml.jackson.databind.ObjectMapper; import com.cisd.tms.security.internal.InternalApiAuthInterceptor;
//import org.junit.jupiter.api.Assertions; import com.fasterxml.jackson.databind.ObjectMapper;
//import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Assertions;
//import org.springframework.mock.web.MockHttpServletRequest; import org.junit.jupiter.api.Test;
//import org.springframework.mock.web.MockHttpServletResponse; import org.mockito.Mockito;
//import org.springframework.web.method.HandlerMethod; import org.springframework.mock.web.MockHttpServletRequest;
// import org.springframework.mock.web.MockHttpServletResponse;
//import java.lang.reflect.Method; import org.springframework.web.method.HandlerMethod;
//
//class InternalAuthorizationInterceptorTest { class InternalAuthorizationInterceptorTest {
//
// @Test @Test
// void shouldAllowRequestWhenHandlerHasNoAuthorizationAnnotations() throws Exception { void shouldRejectWhenRoleDoesNotMatchRequiredRole() throws Exception {
// InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper()); OperationAuditService operationAuditService = Mockito.mock(OperationAuditService.class);
// MockHttpServletRequest request = new MockHttpServletRequest("GET", "/api/v1/public"); InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper(), operationAuditService);
// MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/enable");
// request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN");
// boolean allowed = interceptor.preHandle(request, response, handler("openEndpoint")); request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL");
// MockHttpServletResponse response = new MockHttpServletResponse();
// Assertions.assertTrue(allowed);
// } boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
//
// @Test Assertions.assertFalse(allowed);
// void shouldRejectWhenRoleDoesNotMatchRequiredRole() throws Exception { Assertions.assertEquals(403, response.getStatus());
// InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper()); Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.FORBIDDEN.getCode()));
// MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/enable"); Assertions.assertTrue(response.getContentAsString().contains("role not allowed"));
// request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "AUDIT_ADMIN"); Mockito.verify(operationAuditService).recordDeniedLog(Mockito.eq(request), Mockito.any(HandlerMethod.class), Mockito.eq("role not allowed"));
// request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL"); }
// MockHttpServletResponse response = new MockHttpServletResponse();
// @Test
// boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint")); void shouldRejectWhenAuthLevelDoesNotMatchRequirement() throws Exception {
// OperationAuditService operationAuditService = Mockito.mock(OperationAuditService.class);
// Assertions.assertFalse(allowed); InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper(), operationAuditService);
// Assertions.assertEquals(403, response.getStatus()); MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password");
// Assertions.assertTrue(response.getContentAsString().contains("role not allowed")); request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN");
// } request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "LIMITED");
// MockHttpServletResponse response = new MockHttpServletResponse();
// @Test
// void shouldRejectWhenAuthLevelDoesNotMatchRequirement() throws Exception { boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
// InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper());
// MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password"); Assertions.assertFalse(allowed);
// request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN"); Assertions.assertEquals(403, response.getStatus());
// request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "LIMITED"); Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.FORBIDDEN.getCode()));
// MockHttpServletResponse response = new MockHttpServletResponse(); Assertions.assertTrue(response.getContentAsString().contains("auth level not allowed"));
// Mockito.verify(operationAuditService).recordDeniedLog(Mockito.eq(request), Mockito.any(HandlerMethod.class), Mockito.eq("auth level not allowed"));
// boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint")); }
//
// Assertions.assertFalse(allowed); @Test
// Assertions.assertEquals(403, response.getStatus()); void shouldAllowWhenRoleAndAuthLevelMatch() throws Exception {
// Assertions.assertTrue(response.getContentAsString().contains("auth level not allowed")); OperationAuditService operationAuditService = Mockito.mock(OperationAuditService.class);
// } InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper(), operationAuditService);
// MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password");
// @Test request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN");
// void shouldAllowWhenRoleAndAuthLevelMatch() throws Exception { request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL");
// InternalAuthorizationInterceptor interceptor = new InternalAuthorizationInterceptor(new ObjectMapper()); MockHttpServletResponse response = new MockHttpServletResponse();
// MockHttpServletRequest request = new MockHttpServletRequest("POST", "/api/v1/auth/roles/AUDIT_ADMIN/reset-password");
// request.setAttribute(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "KEY_ADMIN"); boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint"));
// request.setAttribute(InternalApiAuthInterceptor.ATTR_AUTH_LEVEL, "FULL");
// MockHttpServletResponse response = new MockHttpServletResponse(); Assertions.assertTrue(allowed);
// Mockito.verifyNoInteractions(operationAuditService);
// boolean allowed = interceptor.preHandle(request, response, handler("keyAdminFullEndpoint")); }
//
// Assertions.assertTrue(allowed); private static HandlerMethod handler(String methodName) throws NoSuchMethodException {
// } DemoController controller = new DemoController();
// return new HandlerMethod(controller, DemoController.class.getDeclaredMethod(methodName));
// private HandlerMethod handler(String methodName) throws NoSuchMethodException { }
// DemoController controller = new DemoController();
// Method method = DemoController.class.getDeclaredMethod(methodName); @SuppressWarnings("unused")
// return new HandlerMethod(controller, method); private static class DemoController {
// }
// @RequireInternalAuth(role = com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN, authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.FULL)
// @SuppressWarnings("unused") public void keyAdminFullEndpoint() {
// private static class DemoController { }
// }
// public void openEndpoint() { }
// }
//
// @RequireInternalAuth(role = com.cisd.tms.modules.auth.enums.RoleCode.KEY_ADMIN, authLevel = com.cisd.tms.modules.auth.enums.AuthLevel.FULL)
// public void keyAdminFullEndpoint() {
// }
// }
//}
@@ -1,5 +1,6 @@
package com.cisd.tms.modules.auth.service; package com.cisd.tms.modules.auth.service;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException; import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.auth.dto.CaptchaResponse; import com.cisd.tms.modules.auth.dto.CaptchaResponse;
import com.cisd.tms.modules.auth.dto.CurrentUserResponse; import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
@@ -193,6 +194,31 @@ class AuthServiceTest {
Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt()); Assertions.assertEquals(LocalDateTime.of(2026, 3, 23, 2, 0), changed.getLastActiveAt());
} }
@Test
void shouldThrowSessionInvalidWhenChangingPasswordWithExpiredSession() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
roleAccounts.save(activeRole(RoleCode.KEY_ADMIN, "12345678", "ROLE-SALT-K", 0, null, true));
AuthSessionEntity expiredSession = session("token-expired-001", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name());
expiredSession.setExpiresAt(LocalDateTime.of(2026, 3, 23, 1, 59));
sessions.save(expiredSession);
AuthService service = newAuthService(
roleAccounts,
new InMemoryAuthUserAccountRepository(),
sessions,
new InMemoryRoleUkeyBindingRepository(),
FIXED_CLOCK,
() -> "unused"
);
BizException exception = Assertions.assertThrows(BizException.class,
() -> service.changePassword("token-expired-001", "12345678", "87654321"));
Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode());
Assertions.assertEquals("session expired", exception.getMessage());
}
@Test @Test
void shouldIssueCaptchaAndAcceptPasswordLoginWithValidCaptcha() { void shouldIssueCaptchaAndAcceptPasswordLoginWithValidCaptcha() {
InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository(); InMemoryRoleAccountRepository roleAccounts = new InMemoryRoleAccountRepository();
@@ -1,5 +1,7 @@
package com.cisd.tms.modules.log.controller; package com.cisd.tms.modules.log.controller;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.GlobalExceptionHandler;
import com.cisd.tms.modules.log.dto.AuditLogReviewRequest; import com.cisd.tms.modules.log.dto.AuditLogReviewRequest;
import com.cisd.tms.modules.log.enums.AuditResult; import com.cisd.tms.modules.log.enums.AuditResult;
import com.cisd.tms.modules.log.service.OperationAuditService; import com.cisd.tms.modules.log.service.OperationAuditService;
@@ -34,6 +36,7 @@ class OperationAuditControllerTest {
mockMvc = MockMvcBuilders mockMvc = MockMvcBuilders
.standaloneSetup(new OperationAuditController(auditService)) .standaloneSetup(new OperationAuditController(auditService))
.setControllerAdvice(new GlobalExceptionHandler())
.build(); .build();
} }
@@ -64,8 +67,8 @@ class OperationAuditControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(req)) .content(objectMapper.writeValueAsString(req))
.requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SYS_ADMIN")) .requestAttr(InternalApiAuthInterceptor.ATTR_ROLE_CODE, "SYS_ADMIN"))
.andExpect(status().isOk()) .andExpect(status().isForbidden())
.andExpect(jsonPath("$.code").value(403)) .andExpect(jsonPath("$.code").value(ErrorCode.FORBIDDEN.getCode()))
.andExpect(jsonPath("$.msg").value("权限不足:仅审计管理员可执行此操作")); .andExpect(jsonPath("$.msg").value("权限不足:仅审计管理员可执行此操作"));
} }
} }
@@ -9,6 +9,7 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
class LmkControllerTest { class LmkControllerTest {
@@ -48,7 +49,8 @@ class LmkControllerTest {
] ]
} }
""")) """))
.andExpect(status().isOk()); .andExpect(status().isBadRequest())
.andExpect(jsonPath("$.code").value(4001));
Mockito.verifyNoInteractions(lmkService); Mockito.verifyNoInteractions(lmkService);
} }
@@ -6,6 +6,7 @@ import com.cisd.tms.modules.security.replay.enums.ReplayCheckStatus;
import com.cisd.tms.modules.security.replay.enums.ReplayScope; import com.cisd.tms.modules.security.replay.enums.ReplayScope;
import com.cisd.tms.modules.security.replay.exception.ReplayProtectionException; import com.cisd.tms.modules.security.replay.exception.ReplayProtectionException;
import com.cisd.tms.modules.security.replay.service.ReplayProtectionService; import com.cisd.tms.modules.security.replay.service.ReplayProtectionService;
import com.cisd.tms.common.enums.ErrorCode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest; import java.security.MessageDigest;
@@ -122,6 +123,7 @@ class InternalApiReplayInterceptorTest {
Assertions.assertFalse(allowed); Assertions.assertFalse(allowed);
Assertions.assertEquals(409, response.getStatus()); Assertions.assertEquals(409, response.getStatus());
Assertions.assertTrue(response.getContentAsString().contains("replayed nonce")); Assertions.assertTrue(response.getContentAsString().contains("replayed nonce"));
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.CONFLICT.getCode()));
} }
@Test @Test
@@ -146,6 +148,7 @@ class InternalApiReplayInterceptorTest {
Assertions.assertFalse(allowed); Assertions.assertFalse(allowed);
Assertions.assertEquals(503, response.getStatus()); Assertions.assertEquals(503, response.getStatus());
Assertions.assertTrue(response.getContentAsString().contains("replay protection unavailable")); Assertions.assertTrue(response.getContentAsString().contains("replay protection unavailable"));
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.SERVICE_UNAVAILABLE.getCode()));
} }
private static HandlerMethod protectedHandler(String methodName) throws NoSuchMethodException { private static HandlerMethod protectedHandler(String methodName) throws NoSuchMethodException {
@@ -1,5 +1,6 @@
package com.cisd.tms.security.openapi; package com.cisd.tms.security.openapi;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.config.properties.TmsSecurityProperties; import com.cisd.tms.common.config.properties.TmsSecurityProperties;
import com.cisd.tms.common.util.HmacUtil; import com.cisd.tms.common.util.HmacUtil;
import com.cisd.tms.modules.security.replay.dto.ReplayCheckRequest; import com.cisd.tms.modules.security.replay.dto.ReplayCheckRequest;
@@ -115,8 +116,9 @@ class OpenApiSignAuthInterceptorTest {
boolean allowed = interceptor.preHandle(request, response, new Object()); boolean allowed = interceptor.preHandle(request, response, new Object());
Assertions.assertFalse(allowed); Assertions.assertFalse(allowed);
Assertions.assertEquals(401, response.getStatus()); Assertions.assertEquals(409, response.getStatus());
Assertions.assertTrue(response.getContentAsString().contains("replayed nonce")); Assertions.assertTrue(response.getContentAsString().contains("replayed nonce"));
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.CONFLICT.getCode()));
} }
@Test @Test
@@ -144,8 +146,9 @@ class OpenApiSignAuthInterceptorTest {
boolean allowed = interceptor.preHandle(request, response, new Object()); boolean allowed = interceptor.preHandle(request, response, new Object());
Assertions.assertFalse(allowed); Assertions.assertFalse(allowed);
Assertions.assertEquals(401, response.getStatus()); Assertions.assertEquals(503, response.getStatus());
Assertions.assertTrue(response.getContentAsString().contains("replay protection unavailable")); Assertions.assertTrue(response.getContentAsString().contains("replay protection unavailable"));
Assertions.assertTrue(response.getContentAsString().contains("\"code\":" + ErrorCode.SERVICE_UNAVAILABLE.getCode()));
} }
private static TmsSecurityProperties securityProperties(String appId, String secret, long skewSeconds) { private static TmsSecurityProperties securityProperties(String appId, String secret, long skewSeconds) {