如何拦截Spring WebFlux WebClient的请求响应并获取头与体?
WebClient拦截请求/响应并计算HMAC的实现方案
核心思路
通过ExchangeFilterFunction结合请求/响应装饰器,既捕获头信息,又缓存请求/响应体(解决流只能读取一次的问题),进而完成HMAC的计算与验证。
1. 捕获请求体(解决BodyInsert无法直接读取问题)
WebClient的ClientHttpRequest.body()返回的BodyInsert是延迟加载的请求体,无法直接读取。需要通过ClientHttpRequestDecorator拦截请求体的写入过程,将内容缓存为字节数组:
private ClientHttpRequest decorateRequest(ClientHttpRequest request) { return new ClientHttpRequestDecorator(request) { private final ByteArrayOutputStream cachedBody = new ByteArrayOutputStream(); @Override public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { // 缓存请求体字节 return super.writeWith(DataBufferUtils.join(body) .doOnNext(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); cachedBody.write(bytes); DataBufferUtils.release(dataBuffer); }) .map(dataBuffer -> { // 重新生成DataBuffer,保证原请求能正常发送 return bufferFactory().wrap(cachedBody.toByteArray()); })); } // 提供获取缓存请求体的方法 public byte[] getCachedBody() { return cachedBody.toByteArray(); } }; }
2. 捕获响应体(解决bodyToMono后流无法复用问题)
响应体的流同样只能读取一次,需要用ClientHttpResponseDecorator缓存响应体内容,确保获取内容后不影响后续处理:
private ClientHttpResponse decorateResponse(ClientHttpResponse response) { return new ClientHttpResponseDecorator(response) { private byte[] cachedBody; @Override public Flux<DataBuffer> getBody() { if (cachedBody != null) { // 已缓存,直接返回缓存的内容 return Flux.just(bufferFactory().wrap(cachedBody)); } // 缓存响应体 return super.getBody() .doOnNext(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); cachedBody = bytes; DataBufferUtils.release(dataBuffer); }) .map(dataBuffer -> bufferFactory().wrap(cachedBody)); } // 提供获取缓存响应体的方法 public byte[] getCachedBody() { return cachedBody; } }; }
3. 完整的ExchangeFilterFunction实现
将上述装饰器整合到Filter中,获取头信息和缓存的体内容,完成HMAC计算与验证:
public ExchangeFilterFunction hmacFilter() { return (request, next) -> { // 装饰请求,缓存请求体 ClientHttpRequest decoratedRequest = decorateRequest(request); // 发送请求前,获取请求头和缓存的请求体计算HMAC return next.exchange(decoratedRequest) .map(response -> { // 装饰响应,缓存响应体 ClientHttpResponse decoratedResponse = decorateResponse(response); // 获取响应头和缓存的响应体验证HMAC String responseHmac = decoratedResponse.getHeaders().getFirst("X-HMAC"); byte[] responseBody = ((ClientHttpResponseDecorator) decoratedResponse).getCachedBody(); // 这里替换为你的HMAC验证逻辑 boolean isValid = verifyHmac(responseBody, responseHmac); if (!isValid) { throw new RuntimeException("HMAC validation failed"); } return decoratedResponse; }) .doOnSubscribe(subscription -> { // 发送请求前计算请求HMAC并添加到请求头 byte[] requestBody = ((ClientHttpRequestDecorator) decoratedRequest).getCachedBody(); String requestHmac = calculateHmac(requestBody); decoratedRequest.getHeaders().add("X-HMAC", requestHmac); }); }; } // 示例HMAC计算方法 private String calculateHmac(byte[] body) { // 替换为你的HMAC计算逻辑(比如使用SecretKeySpec和Mac类) try { Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec("your-secret-key".getBytes(), "HmacSHA256"); mac.init(secretKey); byte[] hmacBytes = mac.doFinal(body); return Base64.getEncoder().encodeToString(hmacBytes); } catch (NoSuchAlgorithmException | InvalidKeyException e) { throw new RuntimeException(e); } } // 示例HMAC验证方法 private boolean verifyHmac(byte[] body, String receivedHmac) { String calculatedHmac = calculateHmac(body); return calculatedHmac.equals(receivedHmac); }
4. 将Filter添加到WebClient
创建WebClient时注册该Filter:
@Bean public WebClient webClient() { return WebClient.builder() .filter(hmacFilter()) .build(); }
关键说明
- 之所以不用Encoder/Decoder:这类组件仅负责序列化/反序列化,无法直接获取请求/响应的头信息,无法满足HMAC计算需要同时用头+体的场景。
- 请求/响应装饰器的核心作用是缓存流内容,因为Reactor的流是一次性消费的,缓存后既可以读取内容计算HMAC,又能保证原请求/响应的正常处理。
内容的提问来源于stack exchange,提问作者oz3__m4
相关产品推荐
相关产品推荐

