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 a7ac0204..6a086a19 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,182 @@ @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" + ); + + 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 String[] SKIP_PATH_PREFIXES = { + "/actuator", + "/favicon.ico", + "/swagger", + "/v3/api-docs", + "/health-check" + }; + + 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(); + if (shouldSkip(uri)) { + return; + } - StringBuilder logMsg = new StringBuilder(); - logMsg.append("\n[API REQUEST] ").append(method).append(" ").append(url).append(queryString).append("\n"); + long tookMs = (System.nanoTime() - startNs) / 1_000_000; + String query = maskQueryString(request.getQueryString()); - logMsg.append(" > [HEADERS]\n"); - Collections.list(request.getHeaderNames()).forEach(headerName -> - logMsg.append(" - ").append(headerName).append(": ").append(request.getHeader(headerName)).append("\n") - ); + log.info("[API] {} {}{} -> {} ({}ms) ip={} ua=\"{}\"", + request.getMethod(), + uri, + query, + response.getStatus(), + tookMs, + clientIp(request), + request.getHeader("User-Agent")); - 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"); + 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)); } + } - logMsg.append("--------------------------------------------------------------------------------"); + private boolean shouldSkip(String uri) { + for (String prefix : SKIP_PATH_PREFIXES) { + if (uri.startsWith(prefix)) { + return true; + } + } + return false; + } - log.info(logMsg.toString()); + private String maskQueryString(String queryString) { + if (queryString == null || queryString.isBlank()) { + return ""; + } + + 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(); + } + + 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 responseBody(ContentCachingResponseWrapper response) { + return formatBody(response.getContentType(), response.getContentAsByteArray()); + } + + 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; } } 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; }