From cb324b7c56f8b6dce2054d582300e29d2b689331 Mon Sep 17 00:00:00 2001 From: Jansoon Date: Thu, 18 Jun 2026 21:53:25 +0900 Subject: [PATCH 1/3] =?UTF-8?q?refactor:=20=EC=9A=94=EC=B2=AD=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=EC=B5=9C=EC=A0=81=ED=99=94=20=EB=B0=8F=20=EB=AF=BC?= =?UTF-8?q?=EA=B0=90=20=EC=A0=95=EB=B3=B4=20=EB=A7=88=EC=8A=A4=ED=82=B9=20?= =?UTF-8?q?(#237)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/fillter/RequestLoggingFilter.java | 166 +++++++++++++++--- 1 file changed, 144 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java b/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java index a7ac0204..12401031 100644 --- a/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java +++ b/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java @@ -2,11 +2,14 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import java.util.Collections; +import java.util.Enumeration; +import java.util.Set; +import java.util.regex.Pattern; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.util.ContentCachingResponseWrapper; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -18,39 +21,158 @@ @Component public class RequestLoggingFilter extends OncePerRequestFilter { + private static final Set SENSITIVE_HEADERS = Set.of( + "authorization", + "cookie", + "set-cookie", + "proxy-authorization", + "x-auth-token", + "x-api-key" + ); + + private static final Set SENSITIVE_BODY_FIELDS = Set.of( + "email", + "password", + "accessToken", + "refreshToken", + "token", + "secret" + ); + + private static final Set SENSITIVE_QUERY_PARAMS = Set.of( + "code", + "state" + ); + + private static final Pattern JWT_PATTERN = + Pattern.compile("eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"); + + private static final int MAX_BODY_LENGTH = 2000; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - ContentCachingRequestWrapper wrappingRequest = new ContentCachingRequestWrapper(request); - filterChain.doFilter(wrappingRequest, response); + HttpServletRequest requestToUse = isLoggableBody(request.getContentType()) + ? new ContentCachingRequestWrapper(request) + : request; + + ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response); - logRequestDetails(wrappingRequest); + long startNs = System.nanoTime(); + try { + filterChain.doFilter(requestToUse, responseWrapper); + } finally { + logRequestDetails(requestToUse, responseWrapper, startNs); + responseWrapper.copyBodyToResponse(); + } } - private void logRequestDetails(ContentCachingRequestWrapper request) { - String method = request.getMethod(); - String url = request.getRequestURI(); - String queryString = request.getQueryString() != null ? "?" + request.getQueryString() : ""; + private void logRequestDetails(HttpServletRequest request, ContentCachingResponseWrapper response, long startNs) { + String uri = request.getRequestURI(); + + long tookMs = (System.nanoTime() - startNs) / 1_000_000; + String query = maskQueryString(request.getQueryString()); - StringBuilder logMsg = new StringBuilder(); - logMsg.append("\n[API REQUEST] ").append(method).append(" ").append(url).append(queryString).append("\n"); + log.info("[API] {} {}{} -> {} ({}ms) ip={} ua=\"{}\"", + request.getMethod(), + uri, + query, + response.getStatus(), + tookMs, + clientIp(request), + request.getHeader("User-Agent")); - logMsg.append(" > [HEADERS]\n"); - Collections.list(request.getHeaderNames()).forEach(headerName -> - logMsg.append(" - ").append(headerName).append(": ").append(request.getHeader(headerName)).append("\n") - ); + if (log.isDebugEnabled()) { + log.debug("[API REQUEST BODY] {} {} body={}", request.getMethod(), uri, requestBody(request)); + log.debug("[API HEADERS] {} {} {}", request.getMethod(), uri, maskedHeaders(request)); + } + } - byte[] content = request.getContentAsByteArray(); - if (content.length > 0) { - String body = new String(content, StandardCharsets.UTF_8); - logMsg.append(" > [Body Data] : ").append(body.trim()).append("\n"); - } else { - logMsg.append(" > [Body Data] : (Empty)\n"); + private String maskQueryString(String queryString) { + if (queryString == null || queryString.isBlank()) { + return ""; } - logMsg.append("--------------------------------------------------------------------------------"); + StringBuilder sb = new StringBuilder("?"); + String[] pairs = queryString.split("&"); + for (int i = 0; i < pairs.length; i++) { + String pair = pairs[i]; + int eq = pair.indexOf('='); + if (eq > 0) { + String key = pair.substring(0, eq); + if (SENSITIVE_QUERY_PARAMS.contains(key.toLowerCase())) { + sb.append(key).append("=***"); + } else { + sb.append(pair); + } + } else { + sb.append(pair); + } + if (i < pairs.length - 1) { + sb.append("&"); + } + } + return sb.toString(); + } - log.info(logMsg.toString()); + private String clientIp(HttpServletRequest request) { + String forwarded = request.getHeader("X-Forwarded-For"); + if (forwarded != null && !forwarded.isBlank()) { + return forwarded.split(",")[0].trim(); + } + String realIp = request.getHeader("X-Real-IP"); + if (realIp != null && !realIp.isBlank()) { + return realIp; + } + return request.getRemoteAddr(); } + + private String maskedHeaders(HttpServletRequest request) { + StringBuilder sb = new StringBuilder(); + Enumeration names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + String value = SENSITIVE_HEADERS.contains(name.toLowerCase()) + ? "***MASKED***" + : request.getHeader(name); + sb.append(name).append("=").append(value).append("; "); + } + return sb.toString(); + } + + private String requestBody(HttpServletRequest request) { + if (request instanceof ContentCachingRequestWrapper wrapper) { + return formatBody(wrapper.getContentType(), wrapper.getContentAsByteArray()); + } + return "(skipped, content-type=" + request.getContentType() + ")"; + } + + private String formatBody(String contentType, byte[] content) { + if (!isLoggableBody(contentType)) { + return "(skipped, content-type=" + contentType + ")"; + } + if (content.length == 0) { + return "(empty)"; + } + String body = maskBody(new String(content, StandardCharsets.UTF_8)); + if (body.length() > MAX_BODY_LENGTH) { + return body.substring(0, MAX_BODY_LENGTH) + "...(truncated, total " + body.length() + " chars)"; + } + return body; + } + + private boolean isLoggableBody(String contentType) { + return contentType != null && (contentType.contains("json") || contentType.startsWith("text/")); + } + + private String maskBody(String body) { + String masked = body; + for (String field : SENSITIVE_BODY_FIELDS) { + masked = masked.replaceAll("(\"" + field + "\"\\s*:\\s*)\"[^\"]*\"", "$1\"***\""); + } + masked = JWT_PATTERN.matcher(masked).replaceAll("***JWT***"); + return masked; + } + } From c885f15eebf2bb226d21281c187da8f68cdc2471 Mon Sep 17 00:00:00 2001 From: Jansoon Date: Thu, 18 Jun 2026 22:04:17 +0900 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=EC=9A=B4=EC=98=81=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=97=90=EC=84=9C=20=EC=8A=A4=EC=BA=90=EB=84=88=C2=B7?= =?UTF-8?q?=EC=A0=95=EC=A0=81=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EC=A0=9C=EC=99=B8=20(#240)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../error/handler/GlobalExceptionHandler.java | 5 +---- .../global/fillter/RequestLoggingFilter.java | 21 ++++++++++++++++++- .../security/jwt/JwtTokenProvider.java | 1 - 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/sofa/linkiving/global/error/handler/GlobalExceptionHandler.java b/src/main/java/com/sofa/linkiving/global/error/handler/GlobalExceptionHandler.java index 57da09d0..a1aadb2c 100644 --- a/src/main/java/com/sofa/linkiving/global/error/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/sofa/linkiving/global/error/handler/GlobalExceptionHandler.java @@ -25,10 +25,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(NoResourceFoundException.class) public ResponseEntity handleNoResourceFound(NoResourceFoundException exception) { - if (exception.getResourcePath().contains("favicon")) { - return ResponseEntity.notFound().build(); - } - log.error("No static resource {}", exception.getResourcePath(), exception); + log.debug("No static resource: {}", exception.getResourcePath()); return ResponseEntity.notFound().build(); } diff --git a/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java b/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java index 12401031..15051c1b 100644 --- a/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java +++ b/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java @@ -47,6 +47,14 @@ public class RequestLoggingFilter extends OncePerRequestFilter { private static final Pattern JWT_PATTERN = Pattern.compile("eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+"); + private static final String[] SKIP_PATH_PREFIXES = { + "/actuator", + "/favicon.ico", + "/swagger", + "/v3/api-docs", + "/health-check" + }; + private static final int MAX_BODY_LENGTH = 2000; @Override @@ -70,6 +78,9 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse private void logRequestDetails(HttpServletRequest request, ContentCachingResponseWrapper response, long startNs) { String uri = request.getRequestURI(); + if (shouldSkip(uri)) { + return; + } long tookMs = (System.nanoTime() - startNs) / 1_000_000; String query = maskQueryString(request.getQueryString()); @@ -89,6 +100,15 @@ private void logRequestDetails(HttpServletRequest request, ContentCachingRespons } } + private boolean shouldSkip(String uri) { + for (String prefix : SKIP_PATH_PREFIXES) { + if (uri.startsWith(prefix)) { + return true; + } + } + return false; + } + private String maskQueryString(String queryString) { if (queryString == null || queryString.isBlank()) { return ""; @@ -174,5 +194,4 @@ private String maskBody(String body) { masked = JWT_PATTERN.matcher(masked).replaceAll("***JWT***"); return masked; } - } diff --git a/src/main/java/com/sofa/linkiving/security/jwt/JwtTokenProvider.java b/src/main/java/com/sofa/linkiving/security/jwt/JwtTokenProvider.java index 68daff6d..0d109775 100644 --- a/src/main/java/com/sofa/linkiving/security/jwt/JwtTokenProvider.java +++ b/src/main/java/com/sofa/linkiving/security/jwt/JwtTokenProvider.java @@ -110,7 +110,6 @@ public String resolveToken(HttpServletRequest request) { return cookie.getValue(); } - log.warn("Token not found (missing in both header and cookie)"); return null; } From 81df37a902cf0e2ffad1361cafcb6f33d0943541 Mon Sep 17 00:00:00 2001 From: Jansoon Date: Thu, 18 Jun 2026 22:06:10 +0900 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=EC=9D=91=EB=8B=B5=20=EB=B3=B8?= =?UTF-8?q?=EB=AC=B8=20=EB=A1=9C=EA=B9=85=20=EC=B6=94=EA=B0=80=20(#244)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../linkiving/global/fillter/RequestLoggingFilter.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java b/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java index 15051c1b..6a086a19 100644 --- a/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java +++ b/src/main/java/com/sofa/linkiving/global/fillter/RequestLoggingFilter.java @@ -35,8 +35,7 @@ public class RequestLoggingFilter extends OncePerRequestFilter { "password", "accessToken", "refreshToken", - "token", - "secret" + "token" ); private static final Set SENSITIVE_QUERY_PARAMS = Set.of( @@ -97,6 +96,8 @@ private void logRequestDetails(HttpServletRequest request, ContentCachingRespons if (log.isDebugEnabled()) { log.debug("[API REQUEST BODY] {} {} body={}", request.getMethod(), uri, requestBody(request)); log.debug("[API HEADERS] {} {} {}", request.getMethod(), uri, maskedHeaders(request)); + log.debug("[API RESPONSE] {} {} -> {} body={}", request.getMethod(), uri, response.getStatus(), + responseBody(response)); } } @@ -168,6 +169,10 @@ private String requestBody(HttpServletRequest request) { return "(skipped, content-type=" + request.getContentType() + ")"; } + private String responseBody(ContentCachingResponseWrapper response) { + return formatBody(response.getContentType(), response.getContentAsByteArray()); + } + private String formatBody(String contentType, byte[] content) { if (!isLoggableBody(contentType)) { return "(skipped, content-type=" + contentType + ")";