Spring Boot httptrace Actuator记录敏感信息,如何仅记录入站请求?
解决方案
方案一:配置Actuator HttpTrace仅记录入站请求并脱敏敏感信息
1. 禁用出站请求追踪
Spring Boot的HttpTrace功能通过ServerHttpTraceFilter处理入站请求,ClientHttpTraceFilter处理出站请求。要只记录入站请求,可自定义ClientHttpTraceFilter并禁用其追踪逻辑:
import org.springframework.boot.actuate.trace.http.HttpTraceRepository; import org.springframework.boot.actuate.trace.http.ClientHttpTraceFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpRequest; @Configuration public class HttpTraceConfig { @Bean public ClientHttpTraceFilter clientHttpTraceFilter(HttpTraceRepository repository) { return new ClientHttpTraceFilter(repository) { @Override protected boolean shouldTrace(HttpRequest request) { return false; // 禁止追踪所有出站请求 } }; } }
2. 对敏感信息脱敏
即使只记录入站请求,请求体中的敏感字段(如password)也需要脱敏。自定义HttpTraceRepository,在保存追踪数据前替换敏感内容:
import org.springframework.boot.actuate.trace.http.HttpTrace; import org.springframework.boot.actuate.trace.http.InMemoryHttpTraceRepository; import org.springframework.stereotype.Component; @Component public class SensitiveAwareHttpTraceRepository extends InMemoryHttpTraceRepository { private static final String[] SENSITIVE_FIELDS = {"password", "token"}; private static final String MASK = "***"; @Override public void add(HttpTrace trace) { if (trace.getRequest().getBody() != null) { String body = trace.getRequest().getBody(); // 适配表单格式脱敏,可根据JSON等实际格式调整正则 for (String field : SENSITIVE_FIELDS) { body = body.replaceAll(field + "=[^&]*", field + "=" + MASK); body = body.replaceAll("\"" + field + "\":\"[^\"]*\"", "\"" + field + "\":\"" + MASK + "\""); } // 创建新的HttpTrace实例替换脱敏后的请求体 HttpTrace modifiedTrace = new HttpTrace( trace.getTimestamp(), trace.getPrincipal(), trace.getSession(), new HttpTrace.Request( trace.getRequest().getMethod(), trace.getRequest().getUri(), trace.getRequest().getHeaders(), body, trace.getRequest().getRemoteAddress() ), trace.getResponse(), trace.getTimeTaken() ); super.add(modifiedTrace); } else { super.add(trace); } } }
方案二:自定义Filter实现入站请求响应记录
如果需要更灵活的日志格式、脱敏规则控制,可编写自定义Filter:
1. 实现请求响应缓存Wrapper
由于HttpServletRequest/Response的流仅能读取一次,需用Wrapper缓存流内容:
import javax.servlet.ReadListener; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; public class CachedHttpServletRequest extends HttpServletRequestWrapper { private final byte[] cachedBody; public CachedHttpServletRequest(HttpServletRequest request) throws IOException { super(request); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try (ServletInputStream inputStream = request.getInputStream()) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { baos.write(buffer, 0, bytesRead); } cachedBody = baos.toByteArray(); } } @Override public ServletInputStream getInputStream() throws IOException { return new ByteArrayServletInputStream(cachedBody); } private static class ByteArrayServletInputStream extends ServletInputStream { private final ByteArrayInputStream inputStream; public ByteArrayServletInputStream(byte[] buffer) { this.inputStream = new ByteArrayInputStream(buffer); } @Override public boolean isFinished() { return inputStream.available() == 0; } @Override public boolean isReady() { return true; } @Override public void setReadListener(ReadListener listener) { throw new UnsupportedOperationException(); } @Override public int read() throws IOException { return inputStream.read(); } } public byte[] getCachedBody() { return cachedBody; } }
import javax.servlet.ServletOutputStream; import javax.servlet.WriteListener; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import java.io.ByteArrayOutputStream; import java.io.IOException; public class CachedHttpServletResponse extends HttpServletResponseWrapper { private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); private ServletOutputStream outputStream; public CachedHttpServletResponse(HttpServletResponse response) { super(response); } @Override public ServletOutputStream getOutputStream() throws IOException { if (outputStream == null) { outputStream = new CachedServletOutputStream(baos); } return outputStream; } public byte[] getCachedBody() { return baos.toByteArray(); } private static class CachedServletOutputStream extends ServletOutputStream { private final ByteArrayOutputStream baos; public CachedServletOutputStream(ByteArrayOutputStream baos) { this.baos = baos; } @Override public void write(int b) throws IOException { baos.write(b); } @Override public boolean isReady() { return true; } @Override public void setWriteListener(WriteListener listener) { throw new UnsupportedOperationException(); } } }
2. 编写自定义Filter
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.core.annotation.Order; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @Component @WebFilter(urlPatterns = "/*") @Order(1) // 控制Filter执行顺序,数字越小越先执行 public class InboundRequestLoggingFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(InboundRequestLoggingFilter.class); private static final List<String> SENSITIVE_FIELDS = Arrays.asList("password", "token"); private static final String MASK = "***"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) { CachedHttpServletRequest cachedRequest = new CachedHttpServletRequest((HttpServletRequest) request); CachedHttpServletResponse cachedResponse = new CachedHttpServletResponse((HttpServletResponse) response); chain.doFilter(cachedRequest, cachedResponse); // 记录入站请求和响应 logRequest(cachedRequest); logResponse(cachedResponse); } else { chain.doFilter(request, response); } } private void logRequest(CachedHttpServletRequest request) { String requestBody = new String(request.getCachedBody(), StandardCharsets.UTF_8); // 对表单和JSON格式的敏感字段脱敏 for (String field : SENSITIVE_FIELDS) { requestBody = requestBody.replaceAll(field + "=[^&]*", field + "=" + MASK); requestBody = requestBody.replaceAll("\"" + field + "\":\"[^\"]*\"", "\"" + field + "\":\"" + MASK + "\""); } logger.info("Inbound Request | Method: {} | URI: {} | Client IP: {} | Body: {}", request.getMethod(), request.getRequestURI(), request.getRemoteAddr(), requestBody); } private void logResponse(CachedHttpServletResponse response) { String responseBody = new String(response.getCachedBody(), StandardCharsets.UTF_8); // 响应敏感信息脱敏(按需添加) logger.info("Inbound Response | Status: {} | Body: {}", response.getStatus(), responseBody); } }
方案对比
- Actuator HttpTrace方案:复用Spring Boot原生功能,配置简单,但自定义空间有限,日志格式固定。
- 自定义Filter方案:完全可控,可自定义日志格式、脱敏规则、过滤范围,但需要编写更多代码处理流缓存细节。
如果仅需过滤出站请求并简单脱敏,选方案一即可;若需灵活的日志控制,推荐方案二。
内容的提问来源于stack exchange,提问作者Paul Holmes
相关产品推荐
相关产品推荐

