[HTTP 로깅 개선] HttpLoggingFilter에서 요청 및 응답 본문 로깅 기능을 추가하고, 민감 정보 마스킹 로직을 개선하여 가독성을 높임. 요청 및 응답 헤더 로깅 방식도 개선함.

This commit is contained in:
2025-09-01 15:24:55 +09:00
parent 47a59c39ca
commit ff7d69f0b6

View File

@@ -11,106 +11,144 @@ import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper; import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.nio.charset.Charset;
import java.util.Map; import java.nio.charset.StandardCharsets;
import java.util.*;
@Slf4j @Slf4j
@Component @Component
public class HttpLoggingFilter extends OncePerRequestFilter { public class HttpLoggingFilter extends OncePerRequestFilter {
private static final int MAX_LOG_BODY = 10 * 1024; // 10 KB
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException { throws ServletException, IOException {
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
// 요청 정보 로깅 (IP 정보 제거) ContentCachingRequestWrapper wrappedRequest =
log.info("********************************************************************************"); (request instanceof ContentCachingRequestWrapper)
log.info("* HTTP REQUEST START"); ? (ContentCachingRequestWrapper) request
log.info("* Method: {} | URI: {}", request.getMethod(), request.getRequestURI()); : new ContentCachingRequestWrapper(request);
log.info("* Headers: {}", getRequestHeaders(request));
log.info("* Body: {}", getRequestBody(request));
log.info("********************************************************************************");
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper wrappedResponse =
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); (response instanceof ContentCachingResponseWrapper)
? (ContentCachingResponseWrapper) response
: new ContentCachingResponseWrapper(response);
log.info("********************************************************************************");
log.info("* [START] HTTP LOGGING");
log.info("* Method: {} | URI: {}", wrappedRequest.getMethod(), wrappedRequest.getRequestURI());
log.info("* Headers: {}", getRequestHeaders(wrappedRequest));
log.info("********************************************************************************");
try { try {
filterChain.doFilter(wrappedRequest, wrappedResponse); filterChain.doFilter(wrappedRequest, wrappedResponse);
} finally { } finally {
long duration = System.currentTimeMillis() - startTime; long duration = System.currentTimeMillis() - startTime;
String reqBody = extractRequestBody(wrappedRequest);
String resBody = extractResponseBody(wrappedResponse);
// 응답 정보 로깅
log.info("********************************************************************************"); log.info("********************************************************************************");
log.info("* HTTP RESPONSE END"); log.info("* [END] HTTP LOGGING");
log.info("* Method: {} | URI: {}", request.getMethod(), request.getRequestURI()); log.info("* Method: {} | URI: {}", wrappedRequest.getMethod(), wrappedRequest.getRequestURI());
log.info("* Status: {} | Duration: {}ms", wrappedResponse.getStatus(), duration); log.info("* Status: {} | Duration: {}ms", wrappedResponse.getStatus(), duration);
log.info("* Headers: {}", getResponseHeaders(wrappedResponse)); log.info("* Req Body: {}", maskSensitiveData(reqBody));
log.info("* Body: {}", getResponseBody(wrappedResponse)); log.info("* Res Headers: {}", getResponseHeaders(wrappedResponse));
log.info("* Res Body: {}", maskSensitiveData(resBody));
log.info("********************************************************************************"); log.info("********************************************************************************");
wrappedResponse.copyBodyToResponse(); 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) { private String maskSensitiveData(String body) {
if (body == null || body.isEmpty()) return "N/A"; if (body == null || body.isEmpty()) return "N/A";
// JSON 키 기준 토큰/비번류 마스킹 (대소문자 무시)
// 비밀번호, 토큰 등 민감 정보 마스킹 String masked = body
return body.replaceAll("\"password\"\\s*:\\s*\"[^\"]*\"", "\"password\":\"***\"") .replaceAll("(?i)(\"password\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3")
.replaceAll("\"token\"\\s*:\\s*\"[^\"]*\"", "\"token\":\"***\"") .replaceAll("(?i)(\"AccessToken\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3")
.replaceAll("\"refreshToken\"\\s*:\\s*\"[^\"]*\"", "\"refreshToken\":\"***\""); .replaceAll("(?i)(\"RefreshToken\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3")
.replaceAll("(?i)(\"authorization\"\\s*:\\s*\")([^\"]+)(\")", "$1***$3");
return masked;
} }
private String getRequestHeaders(HttpServletRequest request) { private String getRequestHeaders(HttpServletRequest request) {
// 주요 헤더만 (Content-Type, Authorization, User-Agent) Map<String, String> headers = new LinkedHashMap<>();
Map<String, String> headers = new HashMap<>(); // 필요한 헤더만 추리거나, 전부 찍고 민감한 건 마스킹
headers.put("Content-Type", request.getContentType()); Collections.list(request.getHeaderNames()).forEach(name -> {
headers.put("User-Agent", request.getHeader("User-Agent")); String value = request.getHeader(name);
if ("authorization".equalsIgnoreCase(name) && value != null) {
// Authorization 헤더는 마스킹 headers.put(name, value.startsWith("Bearer ") ? "Bearer ***" : "***");
String auth = request.getHeader("Authorization"); } else {
if (auth != null) { headers.put(name, value);
headers.put("Authorization", auth.startsWith("Bearer ") ? "Bearer ***" : "***"); }
} });
return headers.toString(); 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) { private String getResponseHeaders(HttpServletResponse response) {
// 응답 헤더 정보 Map<String, String> headers = new LinkedHashMap<>();
return "Content-Type: " + response.getContentType(); for (String name : response.getHeaderNames()) {
} if ("authorization".equalsIgnoreCase(name)) {
headers.put(name, "***");
private String getResponseBody(HttpServletResponse response) { } else {
try { headers.put(name, String.join(",", response.getHeaders(name)));
ContentCachingResponseWrapper wrapper = (ContentCachingResponseWrapper) response; }
String body = new String(wrapper.getContentAsByteArray());
return maskSensitiveData(body);
} catch (Exception e) {
return "N/A";
} }
if (response.getContentType() != null) {
headers.putIfAbsent("Content-Type", response.getContentType());
}
return headers.toString();
} }
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) { protected boolean shouldNotFilter(HttpServletRequest request) {
String path = request.getRequestURI(); String path = request.getRequestURI();
return path.startsWith("/static/") || return path.startsWith("/static/")
path.startsWith("/css/") || || path.startsWith("/css/")
path.startsWith("/js/") || || path.startsWith("/js/")
path.startsWith("/images/") || || path.startsWith("/images/")
path.equals("/actuator/health") || || path.equals("/favicon.ico")
path.equals("/favicon.ico"); || path.startsWith("/actuator/health");
} }
} }