diff --git a/src/main/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdvice.java b/src/main/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdvice.java index 0ea6d0e..813bc68 100644 --- a/src/main/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdvice.java +++ b/src/main/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdvice.java @@ -16,11 +16,21 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.List; +import java.util.Locale; +import java.util.Set; import java.util.stream.Collectors; @Slf4j @RestControllerAdvice(annotations = RestController.class) public class ExceptionAdvice { + private static final String MASKED_VALUE = "****"; + private static final Set SENSITIVE_FIELD_KEYWORDS = Set.of( + "password", + "token", + "secret", + "authorization", + "credential" + ); @ExceptionHandler(GeneralException.class) public ResponseEntity> handleCustomException(GeneralException e) { @@ -37,7 +47,7 @@ public ResponseEntity> handleValidationException(MethodArgum .map(fe -> String.format("[%s] %s (입력값: %s)", fe.getField(), fe.getDefaultMessage(), - fe.getRejectedValue())) + formatRejectedValue(fe.getField(), fe.getRejectedValue()))) .toList(); log.warn("Validation failed: {}", errors); @@ -51,10 +61,13 @@ public ResponseEntity> handleValidationException(MethodArgum @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity> handleConstraintViolationException(ConstraintViolationException e) { List errors = e.getConstraintViolations().stream() - .map(violation -> String.format("[%s] %s (입력값: %s)", - violation.getPropertyPath().toString(), - violation.getMessage(), - violation.getInvalidValue())) + .map(violation -> { + String propertyPath = violation.getPropertyPath().toString(); + return String.format("[%s] %s (입력값: %s)", + propertyPath, + violation.getMessage(), + formatRejectedValue(propertyPath, violation.getInvalidValue())); + }) .collect(Collectors.toList()); log.warn("Constraint violation: {}", errors); @@ -88,4 +101,21 @@ public ResponseEntity> handleException(Exception e) { .status(code.getHttpStatus()) .body(ApiResponse.onFailure(code, code.getMessage())); } + + static String formatRejectedValue(String field, Object rejectedValue) { + if (isSensitiveField(field)) { + return MASKED_VALUE; + } + return String.valueOf(rejectedValue); + } + + static boolean isSensitiveField(String field) { + if (field == null) { + return false; + } + + String normalizedField = field.toLowerCase(Locale.ROOT); + return SENSITIVE_FIELD_KEYWORDS.stream() + .anyMatch(normalizedField::contains); + } } diff --git a/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java b/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java new file mode 100644 index 0000000..8b001d7 --- /dev/null +++ b/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java @@ -0,0 +1,53 @@ +package com.jobdri.jobdri_api.global.apiPayload.exception.handler; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ExceptionAdviceTest { + + @Test + @DisplayName("민감한 검증 실패 입력값은 키워드별로 마스킹한다") + void maskAllSensitiveKeywords() { + assertThat(ExceptionAdvice.formatRejectedValue("password", "raw-password")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("loginRequest.password", "raw-password")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("refreshToken", "raw-token")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("clientSecret", "raw-secret")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("authorizationHeader", "raw-authorization")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("credentialKey", "raw-credential")) + .isEqualTo("****"); + } + + @Test + @DisplayName("민감한 검증 실패 입력값은 대소문자와 무관하게 마스킹한다") + void maskCaseInsensitiveFields() { + assertThat(ExceptionAdvice.formatRejectedValue("PASSWORD", "raw-password")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("RefreshToken", "raw-token")) + .isEqualTo("****"); + } + + @Test + @DisplayName("민감하지 않은 검증 실패 입력값은 기존처럼 노출한다") + void keepNonSensitiveRejectedValue() { + assertThat(ExceptionAdvice.formatRejectedValue("email", "invalid-email")) + .isEqualTo("invalid-email"); + } + + @Test + @DisplayName("검증 실패 입력값 포맷팅 시 null 입력을 처리한다") + void handleNullInputs() { + assertThat(ExceptionAdvice.formatRejectedValue(null, "raw-value")) + .isEqualTo("raw-value"); + assertThat(ExceptionAdvice.formatRejectedValue("email", null)) + .isEqualTo("null"); + assertThat(ExceptionAdvice.formatRejectedValue("password", null)) + .isEqualTo("****"); + } +}