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

如何在Spring Framework的RestTemplate日志中屏蔽敏感数据

解决RestTemplate调试模式下敏感请求体泄露问题

问题背景

开启调试模式后,Spring Web框架的RestTemplate会打印完整请求体内容。我的应用连接Keycloak获取令牌时,请求参数包含用户名、密码等敏感信息,这些内容会被明文记录在日志中,示例日志:

Writing [{client_id=[client1], grant_type=[password], username=[user1], password=[testit]}]

我试过网上拦截请求屏蔽密码的方案,但因为RestTemplate从HttpEntity中读取请求体参数,该方案无效。以下是我尝试过但未生效的拦截器代码:

public class MaskedLoggingRestTemplate2 extends RestTemplate {

    private Set<String> maskedParameters;
    private HttpEntity<MultiValueMap<String, Object>> requestEntity;

    public MaskedLoggingRestTemplate2(HttpEntity<MultiValueMap<String, Object>> requestEntity) {
        // Initialize the set of parameters to mask
        maskedParameters = new HashSet<>();
        maskedParameters.add("password");
        this.requestEntity = requestEntity;
       // maskedParameters.add("api_key");
        // Add more parameters as needed
    }

    @Override
    protected <T> T doExecute(URI url, @Nullable String uriTemplate, HttpMethod method, RequestCallback requestCallback,
                              ResponseExtractor<T> responseExtractor){
        RequestCallback maskedRequestCallback = new MaskedRequestCallback(requestCallback);
        return super.doExecute(url, uriTemplate, method, maskedRequestCallback, responseExtractor);
    }

    private class MaskedRequestCallback implements RequestCallback {

        private final RequestCallback delegate;

        MaskedRequestCallback(RequestCallback delegate) {
            this.delegate = delegate;
        }

        @Override
        public void doWithRequest(ClientHttpRequest request) throws IOException {
            delegate.doWithRequest(new MaskedClientHttpRequestWrapper(request));
        }
    }

    private class MaskedClientHttpRequestWrapper implements ClientHttpRequest {

        private final ClientHttpRequest delegate;
        private byte[] bodyContent;

        MaskedClientHttpRequestWrapper(ClientHttpRequest delegate) throws IOException {
            this.delegate = delegate;
            this.bodyContent = getMaskedBodyContent();
        }

        @Override
        public HttpMethod getMethod() {
            return delegate.getMethod();
        }

        @Override
        public URI getURI() {
            return delegate.getURI();
        }

        @Override
        public HttpHeaders getHeaders() {
            return delegate.getHeaders();
        }

        @Override
        public OutputStream getBody() throws IOException {

            FastByteArrayOutputStream byteArrayOutputStream = new FastByteArrayOutputStream();
            byteArrayOutputStream.write(bodyContent);
            return byteArrayOutputStream;
            //return delegate.getBody();
        }

        @Override
        public ClientHttpResponse execute() throws IOException {
            ClientHttpResponse response = delegate.execute();
            return new MaskedClientHttpResponseWrapper(response);
        }

        private byte[] getMaskedBodyContent() throws IOException {
//            FastByteArrayOutputStream byteArrayOutputStream = (FastByteArrayOutputStream)delegate.getBody();
//            byte[] originalBody = byteArrayOutputStream.toByteArray();
//            String originalBodyString = new String(originalBody, StandardCharsets.UTF_8);
            MultiValueMap<String, Object> requestEntityBody = requestEntity.getBody();

            //Map<String, String> maskedParameterMap = new LinkedHashMap<>();

            assert requestEntityBody != null;
            LinkedHashMap<String, Object> maskedParameterMap = requestEntityBody.entrySet().stream()
                    .collect(LinkedHashMap::new,
                            (m, entry) -> {
                                m.put(entry.getKey(), maskedParameters.contains(entry.getKey()) ? "******" : entry.getValue());
                            },
                            LinkedHashMap::putAll);

            // Split the body into individual parameters
//            String[] parameters = originalBodyString.split("&");
//            Map<String, String> maskedParameterMap = new LinkedHashMap<>();

            // Mask each parameter as needed
//            for (String parameter : parameters) {
//                String[] keyValue = parameter.split("=");
//                if (keyValue.length == 2) {
//                    String key = keyValue[0];
//                    String value = keyValue[1];
//                    if (maskedParameters.contains(key)) {
//                        value = "********"; // Mask the parameter value
//                    }
//                    maskedParameterMap.put(key, value);
//                }
//            }

            // Recreate the body content with masked parameters
            StringBuilder maskedBodyBuilder = new StringBuilder();
            for (Map.Entry<String, Object> entry : maskedParameterMap.entrySet()) {
                maskedBodyBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
            }
            if (maskedBodyBuilder.length() > 0) {
                maskedBodyBuilder.deleteCharAt(maskedBodyBuilder.length() - 1);
            }

            return maskedBodyBuilder.toString().getBytes(StandardCharsets.UTF_8);
        }
    }

    private class MaskedClientHttpResponseWrapper implements ClientHttpResponse {

        private final ClientHttpResponse delegate;

        MaskedClientHttpResponseWrapper(ClientHttpResponse delegate) {
            this.delegate = delegate;
        }

        @Override
        public HttpStatusCode getStatusCode() throws IOException {
            return delegate.getStatusCode();
        }

        @Override
        public String getStatusText() throws IOException {
            return delegate.getStatusText();
        }

        @Override
        public void close() {

        }

        @Override
        public InputStream getBody() throws IOException {
            return delegate.getBody();
        }

        @Override
        public HttpHeaders getHeaders() {
            return delegate.getHeaders();
        }

        // Implement other delegating methods as needed
    }
}

补充说明:RestTemplate的logBody是私有方法,无法重写,且该方法会被doWithRequest调用。我的需求是在RestTemplate日志中屏蔽密码等敏感数据,或者直接停止打印请求体内容。

可行解决方案

方案1:直接关闭请求体日志输出

调整日志配置,将RestTemplate相关日志的级别从DEBUG提升到INFO或更高,这样就不会打印请求体内容。以logback为例,在logback.xml中添加:

<logger name="org.springframework.web.client.RestTemplate" level="INFO"/>

如果需要保留其他调试日志,也可以更精准地控制:

<logger name="org.springframework.web.client.RestTemplate" level="DEBUG">
    <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
        <evaluator>
            <expression>message contains "Writing"</expression>
        </evaluator>
        <OnMismatch>NEUTRAL</OnMismatch>
        <OnMatch>DENY</OnMatch>
    </filter>
</logger>

方案2:自定义ClientHttpRequestInterceptor实现敏感信息掩码

之前的方案无效是因为没有正确拦截日志打印的时机,改用ClientHttpRequestInterceptor配合自定义日志输出逻辑:

public class MaskingRequestInterceptor implements ClientHttpRequestInterceptor {

    private static final Set<String> SENSITIVE_PARAMS = Set.of("password", "client_secret");

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        // 打印掩码后的请求体
        logMaskedRequest(request, body);
        return execution.execute(request, body);
    }

    private void logMaskedRequest(HttpRequest request, byte[] body) {
        if (body.length == 0 || !request.getHeaders().getContentType().includes(MediaType.APPLICATION_FORM_URLENCODED)) {
            // 非表单请求直接跳过或打印原内容
            return;
        }

        String bodyStr = new String(body, StandardCharsets.UTF_8);
        String[] params = bodyStr.split("&");
        StringBuilder maskedBody = new StringBuilder();

        for (String param : params) {
            String[] keyValue = param.split("=", 2);
            if (keyValue.length != 2) {
                maskedBody.append(param).append("&");
                continue;
            }
            String key = keyValue[0];
            String value = SENSITIVE_PARAMS.contains(key) ? "******" : keyValue[1];
            maskedBody.append(key).append("=").append(value).append("&");
        }

        if (maskedBody.length() > 0) {
            maskedBody.deleteCharAt(maskedBody.length() - 1);
        }

        // 手动打印掩码后的日志
        LoggerFactory.getLogger(RestTemplate.class).debug("Writing [{}]", maskedBody);
    }
}

然后给RestTemplate添加拦截器:

RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add(new MaskingRequestInterceptor());

同时记得关闭RestTemplate自带的DEBUG日志,避免重复打印。

方案3:替换RestTemplate的请求工厂(进阶)

如果需要深度定制,可以自定义ClientHttpRequestFactory,包装请求对象,在getBody()方法中记录掩码后的内容,但这种方式复杂度较高,适合需要精细控制的场景。


内容的提问来源于stack exchange,提问作者P.Sanjay

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 09:00:54