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

如何拦截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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.08 04:25:31