[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 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<String, String> 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<String, String> 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<String, String> 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");
}
}