Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> SENSITIVE_FIELD_KEYWORDS = Set.of(
"password",
"token",
"secret",
"authorization",
"credential"
);

@ExceptionHandler(GeneralException.class)
public ResponseEntity<ApiResponse<Object>> handleCustomException(GeneralException e) {
Expand All @@ -37,7 +47,7 @@ public ResponseEntity<ApiResponse<Object>> 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);
Expand All @@ -51,10 +61,13 @@ public ResponseEntity<ApiResponse<Object>> handleValidationException(MethodArgum
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Object>> handleConstraintViolationException(ConstraintViolationException e) {
List<String> 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);
Expand Down Expand Up @@ -88,4 +101,21 @@ public ResponseEntity<ApiResponse<Object>> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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("****");
}
}
Loading