You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 17:22:51