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