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
相关产品推荐
相关产品推荐

