[HTTP 로깅 개선] HttpLoggingFilter에서 요청 및 응답 본문 로깅 기능을 추가하고, 민감 정보 마스킹 로직을 개선하여 가독성을 높임. 요청 및 응답 헤더 로깅 방식도 개선함.
This commit is contained in:
@@ -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, "***");
|
||||||
|
} else {
|
||||||
|
headers.put(name, String.join(",", response.getHeaders(name)));
|
||||||
}
|
}
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user