【Spring Cloud Gateway专题】四、Spring Cloud Gateway中RequestBody只能获取一次的问题解决方案

1、前言

在网关应用中,如果我想要记录所有请求的参数,然后将请求流转到下游,就会遇到读取RequestBody的问题。无论在Spring5的webflux编程或者普通web编程中,只能从request中获取body一次,后面无法再获取,这个问题怎么解决呢?
网上博客有多种处理办法,对不同的spring cloud gateway版本不一定有用。本文着重说明下版本环境:
spring cloud gateway 2.2.1.RELEASE版,并且在spring cloud gateway 2.2.2.RELEASE也验证通过。spring cloud版本为Hoxton.SR1。

2、普通读取requestbody的示例

controller类代码如下:

package cn.iocoder.springcloud.labx08.gatewaydemo.controller;

import cn.hutool.json.JSONUtil;
import cn.iocoder.springcloud.labx08.gatewaydemo.domain.Blog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("blog")
public class DemoController {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 测试 @Value 注解的属性
     */
    @PostMapping("/blog01")
    public Map<String, Object> blog01(@RequestBody Blog blog,HttpServletRequest request) {
        Map<String, Object> result = new HashMap<>();
        result.put("params" , request.getParameter("token"));
        result.put("bodydata" , JSONUtil.toJsonStr(blog));
        return result;
    }
}

Blog代码如下:

@Data
public class Blog{
    private String title;

    private String content;

    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date publishDate;
}

请求示例
在这里插入图片描述

3、使用spring cloud gateway代理

yaml配置

server:
  port: 8888

spring:
  application:
    name: gateway-application
  cloud:
    # Spring Cloud Gateway 配置项,对应 GatewayProperties 类
    gateway:
      # 路由配置项,对应 RouteDefinition 数组
      routes:
        - id: csdn1 # 路由的编号
          uri: http://localhost:9090
          predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
            - Path=/csdnblog/**
          filters:
            - StripPrefix=1

gateway代码,该代码提供了两种读取body参数的方法,分别为APPLICATION_JSON,APPLICATION_FORM_URLENCODED。处理办法是在向下游请求时,构造新的ServerWebExchange,并将参数传递进去。如果依然使用原ServerWebExchange向下游传递,下游由于请求参数不匹配,直接报404错误。

package cn.iocoder.springcloud.labx08.gatewaydemo.filter;

import cn.iocoder.springcloud.labx08.gatewaydemo.context.GatewayContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

/**
 * Gateway Context Filter
 * @author chenggang
 * @date 2019/01/29
 */

@Slf4j
@Component
public class GatewayContextFilter implements GlobalFilter, Ordered {
    /**
     * default HttpMessageReader
     */
    private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        GatewayContext gatewayContext = new GatewayContext();
        HttpHeaders headers = request.getHeaders();
        gatewayContext.setRequestHeaders(headers);
        gatewayContext.getAllRequestData().addAll(request.getQueryParams());
        /*
         * save gateway context into exchange
         */
        exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT,gatewayContext);
        MediaType contentType = headers.getContentType();
        if(headers.getContentLength()>0){
            if(MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){
                return readBody(exchange, chain,gatewayContext);
            }
            if(MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)){
                return readFormData(exchange, chain,gatewayContext);
            }
        }
        log.debug("[GatewayContext]ContentType:{},Gateway context is set with {}",contentType, gatewayContext);
        return chain.filter(exchange);

    }


    @Override
    public int getOrder() {
        return -2;
    }


    /**
     * ReadFormData
     * @param exchange
     * @param chain
     * @return
     */
    private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext){
        HttpHeaders headers = exchange.getRequest().getHeaders();
        return exchange.getFormData()
                .doOnNext(multiValueMap -> {
                    gatewayContext.setFormData(multiValueMap);
                    gatewayContext.getAllRequestData().addAll(multiValueMap);
                    log.debug("[GatewayContext]Read FormData Success");
                })
                .then(Mono.defer(() -> {
                    Charset charset = headers.getContentType().getCharset();
                    charset = charset == null? StandardCharsets.UTF_8:charset;
                    String charsetName = charset.name();
                    MultiValueMap<String, String> formData = gatewayContext.getFormData();
                    /*
                     * formData is empty just return
                     */
                    if(null == formData || formData.isEmpty()){
                        return chain.filter(exchange);
                    }
                    StringBuilder formDataBodyBuilder = new StringBuilder();
                    String entryKey;
                    List<String> entryValue;
                    try {
                        /*
                         * repackage form data
                         */
                        for (Map.Entry<String, List<String>> entry : formData.entrySet()) {
                            entryKey = entry.getKey();
                            entryValue = entry.getValue();
                            if (entryValue.size() > 1) {
                                for(String value : entryValue){
                                    formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(value, charsetName)).append("&");
                                }
                            } else {
                                formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&");
                            }
                        }
                    }catch (UnsupportedEncodingException e){}
                    /*
                     * substring with the last char '&'
                     */
                    String formDataBodyString = "";
                    if(formDataBodyBuilder.length()>0){
                        formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1);
                    }
                    /*
                     * get data bytes
                     */
                    byte[] bodyBytes =  formDataBodyString.getBytes(charset);
                    int contentLength = bodyBytes.length;
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.putAll(exchange.getRequest().getHeaders());
                    httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
                    /*
                     * in case of content-length not matched
                     */
                    httpHeaders.setContentLength(contentLength);
                    /*
                     * use BodyInserter to InsertFormData Body
                     */
                    BodyInserter<String, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromObject(formDataBodyString);
                    CachedBodyOutputMessage cachedBodyOutputMessage = new CachedBodyOutputMessage(exchange, httpHeaders);
                    log.debug("[GatewayContext]Rewrite Form Data :{}",formDataBodyString);
                    return bodyInserter.insert(cachedBodyOutputMessage,  new BodyInserterContext())
                            .then(Mono.defer(() -> {
                                ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                                        exchange.getRequest()) {
                                    @Override
                                    public HttpHeaders getHeaders() {
                                        return httpHeaders;
                                    }
                                    @Override
                                    public Flux<DataBuffer> getBody() {
                                        return cachedBodyOutputMessage.getBody();
                                    }
                                };
                                return chain.filter(exchange.mutate().request(decorator).build());
                            }));
                }));
    }

    /**
     * ReadJsonBody
     * @param exchange
     * @param chain
     * @return
     */
    private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext){
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .flatMap(dataBuffer -> {
                    /*
                     * read the body Flux<DataBuffer>, and release the buffer
                     * //TODO when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature
                     * see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095
                     */
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
                        DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
                        DataBufferUtils.retain(buffer);
                        return Mono.just(buffer);
                    });
                    log.debug("[GatewayContext]Read JsonBody Success");
                    /*
                     * repackage ServerHttpRequest
                     */
                    ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
                        @Override
                        public Flux<DataBuffer> getBody() {
                            return cachedFlux;
                        }
                    };
                    ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
                    return ServerRequest.create(mutatedExchange, MESSAGE_READERS)
                            .bodyToMono(String.class)
                            .doOnNext(objectValue -> {
                                gatewayContext.setRequestBody(objectValue);
                            }).then(chain.filter(mutatedExchange/*exchange*/));
                });
    }

}

在这里插入图片描述
源码:https://github.com/muziye2013/SpringBoot-Labs 请参考labx-08/labx-08-ex-gateway-demo02,labx-08/labx-08-ex-gateway-demo02-controller两个模块的代码。

主要参考的文章及代码:
关于Spring-webflux编程中body只能获取一次的问题解决方案
SpringCloud Gateway 记录缓存请求Body和Form表单
Spring Cloud Gateway-ServerWebExchange核心方法与请求或者响应内容的修改 注意该文的处理方法只适用于其对应版本
Spring Cloud Gateway 之 Filter,对于filter的讲解很详细。
除此之外,还要学会从spring cloud gateway的issue中寻找问题以及对应的处理办法。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Spring Cloud Gateway 可以通过自定义过滤器来获取请求body)。具步骤如下: 1. 创建一个自定义过滤器类,实现 GatewayFilter 接口。 2. 在过滤器类重写 filter 方法,在该方法获取请求。 3. 在 Spring Cloud Gateway 配置文件配置该过滤器。 示例代码如下: ```java @Component public class MyFilter implements GatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求 ServerHttpRequest request = exchange.getRequest(); Flux<DataBuffer> body = request.getBody(); // 处理请求 // ... // 调用下一个过滤器 return chain.filter(exchange); } } ``` 在 Spring Cloud Gateway 配置文件配置该过滤器: ```yaml spring: cloud: gateway: routes: - id: my_route uri: http://localhost:8080 predicates: - Path=/my_path/** filters: - MyFilter ``` 其,MyFilter 是自定义过滤器类的名称。在 filters 配置指定该过滤器即可。 ### 回答2: Spring Cloud Gateway是一个基于Spring Boot的API网关,它允许开发者以统一的方式管理和路由HTTP请求到多个微服务。在实际开发,有时需要获取HTTP请求body,在Spring Cloud Gateway获取HTTP请求body需要注意以下几点: 1. 所有的Route Predicate都需要配置读取HTTP请求,否则在路由到下游服务时,请求会丢失。 2. 如果请求是将JSON字符串作为参数传递,则需要使用JSON库将字符串转成JSON对象。Spring Cloud Gateway推荐使用与Spring Framework组件集成的Jackson JSON库。 3. HTTP请求body只能读取一次,所以需要配置路由过滤器来实现将读取过的请求保存在请求上下文,以便后续的路由过滤器和路由处理器获取请求。 在Spring Cloud Gateway获取HTTP请求body,可以通过自定义GatewayFilter来实现。下面给出获取HTTP请求的代码示例: ```java public class BodyGatewayFilterFactory extends AbstractGatewayFilterFactory<BodyGatewayFilterFactory.Config> { public BodyGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); String requestBody = new String(bytes, Charset.forName("UTF-8")); exchange.getAttributes().put("requestBody", requestBody); return chain.filter(exchange); }); }; } public static class Config { } } ``` 在上面的代码,使用DataBufferUtils.join()函数将请求存储在字节数组,并通过exchange的setAttribute()方法存储到请求上下文。这样,在后续的路由过滤器和路由处理器就可以通过读取exchange.getAttributes().get("requestBody")来获取HTTP请求body,而无需重新读取请求。 ### 回答3: Spring Cloud Gateway是一个基于Spring Boot的网关。它可以在微服务架构起到路由、负载均衡、API管理等多种作用。 在Spring Cloud Gateway获取请求有两种方式:获取单个请求获取多个请求获取单个请求: 在Spring Cloud Gateway获取单个请求可以使用Exchange对象的getBody()方法。这个方法会返回一个Mono对象,需要使用subscribe()方法来订阅结果。 例如: ```java public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Mono<String> requestBody = exchange.getRequest().getBodyToMono(String.class); requestBody.subscribe(content -> { // 对请求进行操作 }); return chain.filter(exchange); } ``` 上面代码,我们使用getBodyToMono()获取请求,然后使用subscribe()方法来订阅请求的内容。订阅成功后,我们可以对请求进行操作。 获取多个请求: 在Spring Cloud Gateway获取多个请求可以使用GlobalFilter。GlobalFilter是一种全局过滤器,可以对所有的请求进行处理。 我们可以创建一个自定义的GlobalFilter,然后在filter()方法获取请求。 例如: ```java @Component public class MyGlobalFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); Flux<DataBuffer> body = exchange.getRequest().getBody(); return chain.filter(exchange.mutate().request( exchange.getRequest().mutate().body(Flux.just(body)).build()) .build()); } } ``` 上面代码,我们创建了一个MyGlobalFilter类,并实现了GlobalFilter接口。在filter()方法,我们使用getBody()获取请求获取请求后,我们更改了请求的数据,然后使用build()方法创建了一个新的Exchange对象,并返回chain.filter()。 总结: Spring Cloud Gateway可以通过Exchange对象来获取请求。可以使用getBody()方法获取单个请求,也可以使用GlobalFilter获取多个请求。 注意:在Spring Cloud Gateway请求是一个Flux对象。如果需要将请求转换成其他类型,请使用getBodyToMono()方法。由于Flux对象可能包含多个元素,因此在订阅操作时需要注意。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值