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

WebClient如何获取请求体?请求响应日志及敏感信息哈希方案

搞定WebClient请求/响应日志:获取请求体+敏感信息哈希脱敏

嘿,我来帮你解决这个WebClient日志记录的痛点!你遇到的问题很典型:BodyInserter不是存储请求体的容器,而是负责把数据写入请求的逻辑,所以直接调用request.body()拿不到实际内容。下面我给你一步步拆解解决方案,包括捕获请求体、敏感信息脱敏,还有完整的日志实现。

一、为什么拿不到请求体?

先给你理清楚:BodyInserter的作用是将数据写入到ClientHttpRequest,它本身不保存请求体内容。所以咱们得换个思路——拦截写入请求的过程,把内容缓存下来,才能转成字符串。

二、核心实现思路

咱们用ClientHttpRequestDecorator包装原始请求,重写写入方法,在数据写入时把内容缓存到字节数组,之后转成字符串。同时,针对敏感信息(比如密码、token),咱们用哈希算法替换原内容,避免泄露。

三、完整代码实现

1. 敏感信息哈希工具类

先写一个工具类,专门处理敏感字段的哈希替换,支持JSON格式的请求/响应:

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SensitiveDataHasher {
    // 定义需要脱敏的字段,你可以根据自己的需求添加
    private static final Pattern SENSITIVE_FIELDS = Pattern.compile(
        "(?<=\"password\":\")(.*?)(?=\")|(?<=\"token\":\")(.*?)(?=\")|(?<=\"creditCard\":\")(.*?)(?=\")",
        Pattern.CASE_INSENSITIVE
    );

    public static String hashSensitiveData(String content) {
        if (content == null) return null;
        Matcher matcher = SENSITIVE_FIELDS.matcher(content);
        StringBuffer sb = new StringBuffer();
        while (matcher.find()) {
            // 把匹配到的敏感值替换成SHA-256哈希
            matcher.appendReplacement(sb, generateHash(matcher.group()));
        }
        matcher.appendTail(sb);
        return sb.toString();
    }

    private static String generateHash(String value) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hashBytes = md.digest(value.getBytes(StandardCharsets.UTF_8));
            StringBuilder hexString = new StringBuilder();
            for (byte b : hashBytes) {
                String hex = Integer.toHexString(0xff & b);
                if (hex.length() == 1) hexString.append('0');
                hexString.append(hex);
            }
            return hexString.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Failed to hash sensitive value", e);
        }
    }
}

2. 自定义日志Filter

接下来写ExchangeFilterFunction,实现请求体捕获、响应体捕获,还有敏感信息处理:

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestDecorator;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.ClientHttpResponseDecorator;
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;

public class WebClientLoggingFilter implements ExchangeFilterFunction {

    @Override
    public Mono<ClientHttpResponse> filter(ClientRequest request, ExchangeFunction next) {
        // 先记录请求的基本信息:方法、URL、请求头
        HttpMethod method = request.method();
        String url = request.url().toString();
        HttpHeaders headers = request.headers();
        System.out.printf("🔍 Outgoing Request: %s %s%nHeaders: %s%n", method, url, headers);

        // 捕获并记录请求体
        return next.exchange(ClientRequest.from(request)
                        .body((outputMessage, context) -> {
                            // 包装请求,拦截写入操作
                            ClientHttpRequestDecorator requestDecorator = new ClientHttpRequestDecorator(outputMessage) {
                                @Override
                                public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                                    // 把数据流合并成一个DataBuffer,转成字节数组
                                    return DataBufferUtils.join(body)
                                            .map(dataBuffer -> {
                                                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                                                dataBuffer.read(bytes);
                                                DataBufferUtils.release(dataBuffer); // 释放原缓冲区

                                                // 转成字符串并脱敏
                                                String requestBody = new String(bytes, StandardCharsets.UTF_8);
                                                String maskedBody = SensitiveDataHasher.hashSensitiveData(requestBody);
                                                System.out.printf("📝 Request Body: %s%n", maskedBody);

                                                // 重新创建DataBuffer,保证请求能正常发送
                                                return outputMessage.bufferFactory().wrap(bytes);
                                            })
                                            .flatMap(super::writeWith);
                                }
                            };
                            // 执行原始的BodyInserter逻辑
                            return request.body().insert(requestDecorator, context);
                        })
                        .build())
                // 捕获并记录响应体
                .map(response -> {
                    ClientHttpResponseDecorator responseDecorator = new ClientHttpResponseDecorator(response) {
                        @Override
                        public Mono<DataBuffer> getBody() {
                            return super.getBody()
                                    .map(dataBuffer -> {
                                        byte[] bytes = new byte[dataBuffer.readableByteCount()];
                                        dataBuffer.read(bytes);
                                        DataBufferUtils.release(dataBuffer);

                                        // 转成字符串并脱敏
                                        String responseBody = new String(bytes, StandardCharsets.UTF_8);
                                        String maskedResponseBody = SensitiveDataHasher.hashSensitiveData(responseBody);
                                        System.out.printf("✅ Incoming Response: %s%nBody: %s%n", response.getStatusCode(), maskedResponseBody);

                                        // 重新创建DataBuffer,保证响应能正常返回
                                        return response.bufferFactory().wrap(bytes);
                                    });
                        }
                    };
                    return responseDecorator;
                });
    }
}

3. 配置WebClient使用Filter

最后把这个Filter添加到WebClient的构建器里:

// 假设properties是你的配置类实例,包含endpoint信息
WebClient webClient = WebClient.builder()
        .baseUrl(properties.getEndpoint())
        .filter(new WebClientLoggingFilter())
        .build();

四、关键细节提醒

  • 数据流不中断:捕获完请求体/响应体后,一定要重新创建DataBuffer并传递,否则原请求/响应的数据流会中断,导致请求失败。
  • 性能优化:如果你的请求体很大,缓存整个字节数组可能占用过多内存,你可以考虑截断日志(比如只记录前1000个字符),或者用流式处理的方式记录。
  • 适配不同请求类型:上面的示例针对JSON请求,如果是表单请求(FormInserter),你可以调整敏感信息处理的逻辑,比如解析表单参数再哈希敏感字段。
  • 日志框架整合:示例里用了System.out,实际项目中你可以替换成SLF4J/Logback等日志框架,更规范地管理日志。

五、替代方案(可选)

如果你不想自己写Filter,也可以尝试使用Spring官方的spring-boot-starter-actuator配合WebClient的Metrics,但自定义Filter的灵活性更高,能完全控制日志格式和脱敏规则。

内容的提问来源于stack exchange,提问作者Marcin Szulc

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 09:19:38