diff --git a/src/main/java/com/bio/bio_backend/global/filter/HttpLoggingFilter.java b/src/main/java/com/bio/bio_backend/global/filter/HttpLoggingFilter.java index cf94e28..fd525b7 100644 --- a/src/main/java/com/bio/bio_backend/global/filter/HttpLoggingFilter.java +++ b/src/main/java/com/bio/bio_backend/global/filter/HttpLoggingFilter.java @@ -11,106 +11,144 @@ import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.*; @Slf4j @Component public class HttpLoggingFilter extends OncePerRequestFilter { + private static final int MAX_LOG_BODY = 10 * 1024; // 10 KB + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - + long startTime = System.currentTimeMillis(); - - // 요청 정보 로깅 (IP 정보 제거) + + ContentCachingRequestWrapper wrappedRequest = + (request instanceof ContentCachingRequestWrapper) + ? (ContentCachingRequestWrapper) request + : new ContentCachingRequestWrapper(request); + + ContentCachingResponseWrapper wrappedResponse = + (response instanceof ContentCachingResponseWrapper) + ? (ContentCachingResponseWrapper) response + : new ContentCachingResponseWrapper(response); + log.info("********************************************************************************"); - log.info("* HTTP REQUEST START"); - log.info("* Method: {} | URI: {}", request.getMethod(), request.getRequestURI()); - log.info("* Headers: {}", getRequestHeaders(request)); - log.info("* Body: {}", getRequestBody(request)); + log.info("* [START] HTTP LOGGING"); + log.info("* Method: {} | URI: {}", wrappedRequest.getMethod(), wrappedRequest.getRequestURI()); + log.info("* Headers: {}", getRequestHeaders(wrappedRequest)); log.info("********************************************************************************"); - - ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); - ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); - + try { filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { long duration = System.currentTimeMillis() - startTime; - - // 응답 정보 로깅 + String reqBody = extractRequestBody(wrappedRequest); + String resBody = extractResponseBody(wrappedResponse); + log.info("********************************************************************************"); - log.info("* HTTP RESPONSE END"); - log.info("* Method: {} | URI: {}", request.getMethod(), request.getRequestURI()); + log.info("* [END] HTTP LOGGING"); + log.info("* Method: {} | URI: {}", wrappedRequest.getMethod(), wrappedRequest.getRequestURI()); log.info("* Status: {} | Duration: {}ms", wrappedResponse.getStatus(), duration); - log.info("* Headers: {}", getResponseHeaders(wrappedResponse)); - log.info("* Body: {}", getResponseBody(wrappedResponse)); + log.info("* Req Body: {}", maskSensitiveData(reqBody)); + log.info("* Res Headers: {}", getResponseHeaders(wrappedResponse)); + log.info("* Res Body: {}", maskSensitiveData(resBody)); log.info("********************************************************************************"); - + wrappedResponse.copyBodyToResponse(); } } - - // 민감 정보 마스킹 메서드 + + private String extractRequestBody(ContentCachingRequestWrapper wrapper) { + // multipart/form-data 등은 보통 로깅 제외 + String ct = Optional.ofNullable(wrapper.getContentType()).orElse(""); + if (ct.toLowerCase(Locale.ROOT).startsWith("multipart/")) { + return "(multipart skipped)"; + } + + byte[] buf = wrapper.getContentAsByteArray(); + if (buf == null || buf.length == 0) return ""; + Charset cs = resolveCharset(wrapper.getCharacterEncoding()); + String s = new String(buf, cs); + return truncate(s, MAX_LOG_BODY); + } + + private String extractResponseBody(ContentCachingResponseWrapper wrapper) { + byte[] buf = wrapper.getContentAsByteArray(); + if (buf == null || buf.length == 0) return ""; + + // UTF-8로 고정하여 한글 깨짐 방지 + String s = new String(buf, StandardCharsets.UTF_8); + return truncate(s, MAX_LOG_BODY); + } + + private Charset resolveCharset(String enc) { + try { + return (enc != null) ? Charset.forName(enc) : StandardCharsets.UTF_8; + } catch (Exception e) { + return StandardCharsets.UTF_8; + } + } + + private String truncate(String s, int max) { + if (s == null) return "N/A"; + if (s.length() <= max) return s; + return s.substring(0, max) + "...(truncated)"; + } + + // 민감 정보 마스킹 (간단/범용) private String maskSensitiveData(String body) { if (body == null || body.isEmpty()) return "N/A"; - - // 비밀번호, 토큰 등 민감 정보 마스킹 - return body.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"***\"") - .replaceAll("\"token\"\\s*:\\s*\"[^\"]*\"", "\"token\":\"***\"") - .replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"***\""); + // JSON 키 기준 토큰/비번류 마스킹 (대소문자 무시) + String masked = body + .replaceAll("(?i)(\"password\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3") + .replaceAll("(?i)(\"AccessToken\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3") + .replaceAll("(?i)(\"RefreshToken\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3") + .replaceAll("(?i)(\"authorization\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3"); + return masked; } - + private String getRequestHeaders(HttpServletRequest request) { - // 주요 헤더만 (Content-Type, Authorization, User-Agent) - Map headers = new HashMap<>(); - headers.put("Content-Type", request.getContentType()); - headers.put("User-Agent", request.getHeader("User-Agent")); - - // Authorization 헤더는 마스킹 - String auth = request.getHeader("Authorization"); - if (auth != null) { - headers.put("Authorization", auth.startsWith("Bearer ") ? "Bearer ***" : "***"); - } - + Map headers = new LinkedHashMap<>(); + // 필요한 헤더만 추리거나, 전부 찍고 민감한 건 마스킹 + Collections.list(request.getHeaderNames()).forEach(name -> { + String value = request.getHeader(name); + if ("authorization".equalsIgnoreCase(name) && value != null) { + headers.put(name, value.startsWith("Bearer ") ? "Bearer ***" : "***"); + } else { + headers.put(name, value); + } + }); return headers.toString(); } - - private String getRequestBody(HttpServletRequest request) { - try { - ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request; - String body = new String(wrapper.getContentAsByteArray()); - return maskSensitiveData(body); - } catch (Exception e) { - return "N/A"; - } - } - + private String getResponseHeaders(HttpServletResponse response) { - // 응답 헤더 정보 - return "Content-Type: " + response.getContentType(); - } - - private String getResponseBody(HttpServletResponse response) { - try { - ContentCachingResponseWrapper wrapper = (ContentCachingResponseWrapper) response; - String body = new String(wrapper.getContentAsByteArray()); - return maskSensitiveData(body); - } catch (Exception e) { - return "N/A"; + Map headers = new LinkedHashMap<>(); + for (String name : response.getHeaderNames()) { + if ("authorization".equalsIgnoreCase(name)) { + headers.put(name, "***"); + } else { + headers.put(name, String.join(",", response.getHeaders(name))); + } } + if (response.getContentType() != null) { + headers.putIfAbsent("Content-Type", response.getContentType()); + } + return headers.toString(); } - + @Override protected boolean shouldNotFilter(HttpServletRequest request) { String path = request.getRequestURI(); - return path.startsWith("/static/") || - path.startsWith("/css/") || - path.startsWith("/js/") || - path.startsWith("/images/") || - path.equals("/actuator/health") || - path.equals("/favicon.ico"); + return path.startsWith("/static/") + || path.startsWith("/css/") + || path.startsWith("/js/") + || path.startsWith("/images/") + || path.equals("/favicon.ico") + || path.startsWith("/actuator/health"); } }