From 32bd32e9575d14e13eb983f98c9f9a4a48016d66 Mon Sep 17 00:00:00 2001 From: yoonseo Date: Sun, 13 Sep 2026 04:24:11 +0900 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8feat:=20=EB=B3=B4=EC=95=88=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=9D=91=EB=8B=B5=EC=97=90=20errorId=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EB=B0=8F=20=EB=A1=9C=EA=B9=85=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/SecurityConfig.java | 42 ++++++++++++++++--- .../SecurityErrorResponseIntegrationTest.java | 6 ++- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/hanspoon/backend_api/global/config/SecurityConfig.java b/src/main/java/com/hanspoon/backend_api/global/config/SecurityConfig.java index 01359c5..fa09be5 100644 --- a/src/main/java/com/hanspoon/backend_api/global/config/SecurityConfig.java +++ b/src/main/java/com/hanspoon/backend_api/global/config/SecurityConfig.java @@ -7,7 +7,10 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; +import java.util.UUID; import javax.crypto.SecretKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -36,6 +39,8 @@ @EnableMethodSecurity public class SecurityConfig { + private static final Logger log = LoggerFactory.getLogger(SecurityConfig.class); + private static final String[] PUBLIC_ENDPOINTS = { "/", "/error", @@ -104,21 +109,39 @@ public JwtAuthenticationConverter jwtAuthenticationConverter() { @Bean public AuthenticationEntryPoint authenticationEntryPoint(ObjectMapper objectMapper) { - return (request, response, exception) -> - writeProblemDetail(objectMapper, response, HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_TOKEN); + return (request, response, exception) -> { + String errorId = UUID.randomUUID().toString(); + log.warn( + "Authentication rejected: errorId={}, method={}, path={}, exception={}, cause={}", + errorId, + request.getMethod(), + request.getRequestURI(), + exception.getClass().getSimpleName(), + rootCauseName(exception)); + writeProblemDetail(objectMapper, response, HttpStatus.UNAUTHORIZED, ErrorCode.INVALID_TOKEN, errorId); + }; } @Bean public AccessDeniedHandler accessDeniedHandler(ObjectMapper objectMapper) { - return (request, response, exception) -> - writeProblemDetail(objectMapper, response, HttpStatus.FORBIDDEN, ErrorCode.ACCESS_DENIED); + return (request, response, exception) -> { + String errorId = UUID.randomUUID().toString(); + log.warn( + "Access denied: errorId={}, method={}, path={}, exception={}", + errorId, + request.getMethod(), + request.getRequestURI(), + exception.getClass().getSimpleName()); + writeProblemDetail(objectMapper, response, HttpStatus.FORBIDDEN, ErrorCode.ACCESS_DENIED, errorId); + }; } private void writeProblemDetail( ObjectMapper objectMapper, jakarta.servlet.http.HttpServletResponse response, HttpStatus status, - ErrorCode errorCode) + ErrorCode errorCode, + String errorId) throws IOException { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, errorCode.getMessage()); @@ -126,9 +149,18 @@ private void writeProblemDetail( problemDetail.setType(URI.create("https://api.han-spoon.site/problems/" + errorCode.getCode())); problemDetail.setProperty("code", errorCode.getCode()); problemDetail.setProperty("timestamp", OffsetDateTime.now()); + problemDetail.setProperty("errorId", errorId); response.setStatus(status.value()); response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE); objectMapper.writeValue(response.getOutputStream(), problemDetail); } + + private String rootCauseName(Throwable throwable) { + Throwable current = throwable; + while (current.getCause() != null && current.getCause() != current) { + current = current.getCause(); + } + return current.getClass().getSimpleName(); + } } diff --git a/src/test/java/com/hanspoon/backend_api/global/config/SecurityErrorResponseIntegrationTest.java b/src/test/java/com/hanspoon/backend_api/global/config/SecurityErrorResponseIntegrationTest.java index 475c46a..ecf19e7 100644 --- a/src/test/java/com/hanspoon/backend_api/global/config/SecurityErrorResponseIntegrationTest.java +++ b/src/test/java/com/hanspoon/backend_api/global/config/SecurityErrorResponseIntegrationTest.java @@ -35,7 +35,8 @@ void missingTokenReturnsProblemDetail() throws Exception { mockMvc.perform(get(PROTECTED_ENDPOINT)) .andExpect(status().isUnauthorized()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON)) - .andExpect(jsonPath("$.code").value("INVALID_TOKEN")); + .andExpect(jsonPath("$.code").value("INVALID_TOKEN")) + .andExpect(jsonPath("$.errorId").isNotEmpty()); } @Test @@ -45,6 +46,7 @@ void invalidTokenReturnsProblemDetailNotEmptyBody() throws Exception { mockMvc.perform(get(PROTECTED_ENDPOINT).header(HttpHeaders.AUTHORIZATION, "Bearer not-a-jwt")) .andExpect(status().isUnauthorized()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON)) - .andExpect(jsonPath("$.code").value("INVALID_TOKEN")); + .andExpect(jsonPath("$.code").value("INVALID_TOKEN")) + .andExpect(jsonPath("$.errorId").isNotEmpty()); } }