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