Spring Cloud Gateway- Get the body trampling practice

Problem 1: Can’t get the body content

Cause analysis

In the process of use, the content obtained from the filter was always empty. I tried various methods to parse the body content on the Internet, but the results were the same. The body data could not be obtained either dead or alive. Various attempts were made, and it turned out that using different Versions of Spring Boot and Spring Cloud made a big difference.

Best practices

Solution 1: Reduce the version

Springboot version: 2.0.5-RELEASE SpringCloud version: finchley. RELEASE

java.lang.IllegalStateException: Only one connection receive subscriber allowed.Copy the code

The reason is that Spring Boot in version 2.0.5 automatically configures the HiddenHttpMethodFilter filter if WebFlux is used. If you want to use other HTTP methods (e.g. PUT, DELETE, PATCH) can only be represented by a hidden attribute such as (_method=PUT). The HiddenHttpMethodFilter replaces the value of the _method parameter in the POST request with the HTTP request method. However, this results in the body having already been read once, causing subsequent filters to fail to read the body. The solution is to rewrite the HiddenHttpMethodFilter itself to override the original implementation, but the Gateway itself should not do this; the original request should be forwarded downstream as it is.

@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new HiddenHttpMethodFilter() {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
            returnchain.filter(exchange); }}; }Copy the code

This is also the solution currently being proposed by gateway’s official developers.

Option 2: Cache the body content without lowering the version

In later versions, the above method does not work, so you can customize a high priority filter to fetch the body content and cache it, so that the body can only be read once. For the solution, see Question 2.

Problem 2: The body can only be read once

The main online solution to this problem is to get the body, rewrap the request, and then pass the wrapped request. The idea was clear, but the way it was implemented was strange. In the use of the process encountered all kinds of strange problems, such as the first request normal, the second request reported 400 error, so alternating. The final cause was my custom global filter that repackaged the Request and removed it. In view of stepping on the pit more, the following is the author’s best practice in the implementation process.

The core code

import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** * This filter fixes the problem that the body cannot be read repeatedly * there is no need to put the body content in the attribute, Because fetching the body from the attribute still requires a * Flux<DataBuffer> and then a String, it is no different from reading the body directly */ @Component public class CacheBodyGlobalFilter implements Ordered, GlobalFilter { // public static final String CACHE_REQUEST_BODY_OBJECT_KEY ="cachedRequestBodyObject";

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    if (exchange.getRequest().getHeaders().getContentType() == null) {
      return chain.filter(exchange);
    } else {
      return DataBufferUtils.join(exchange.getRequest().getBody())
          .flatMap(dataBuffer -> {
            DataBufferUtils.retain(dataBuffer);
            Flux<DataBuffer> cachedFlux = Flux
                .defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
            ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
                exchange.getRequest()) {
              @Override
              public Flux<DataBuffer> getBody() {
                returncachedFlux; }}; // exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, cachedFlux);return chain.filter(exchange.mutate().request(mutatedRequest).build());
          });
    }
  }

  @Override
  public int getOrder() {
    returnOrdered.HIGHEST_PRECEDENCE; }}Copy the code

CacheBodyGlobalFilter the purpose of the global filter is the original request in the request body content is read, and use a decorator ServerHttpRequestDecorator this request to the request for packaging, rewrite the getBody method, And passes the wrapped request through the filter chain. When a later filter uses exchange.getrequest ().getBody() to retrieve the body, the overloaded getBody method is called to retrieve the first cached body data. This enables multiple reads of the body. It is worth mentioning that the order of this filter is set to order.highest_precedence, which is the highest precedence filter. The reason why the priority is set so high is that some built-in filters may also read the body, which will result in the error that the body can only be read once in our custom filter:

java.lang.IllegalStateException: Only one connection receive subscriber allowed.
	at reactor.ipc.netty.channel.FluxReceive.startReceiver(FluxReceive.java:279)
	at reactor.ipc.netty.channel.FluxReceive.lambda$subscribe$2(FluxReceive.java:129)
	at 
Copy the code

The body can only be read once and the CacheBodyGlobalFilter priority is set to the highest.

import io.netty.buffer.ByteBufAllocator; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequestDecorator; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * @author mjw * @date 2020/3/24 */ @Component @Slf4j public class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String bodyContent = RequestUtil.resolveBodyFromRequest(exchange.getRequest()); // TODO authentication related logicreturn chain.filter(exchange.mutate().build());
    }

    @Override
    public int getOrder()
    {
        return -100;
    }
}
Copy the code

This class is a global filter for custom authentication, and it needs to be explained how to parse after reading the body. Since the Spring Cloud Gateway uses webFlux, the obtained body content is in Flux structure, and the reading method is as follows:

import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.server.reactive.ServerHttpRequest; import reactor.core.publisher.Flux; import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * @author MJW * @date 2020/3/30 */ public class RequestUtil {/** ** @param serverHttpRequest * @return*/ public static String resolveBodyFromRequest(ServerHttpRequest ServerHttpRequest){Flux<DataBuffer> body = serverHttpRequest.getBody(); StringBuilder sb = new StringBuilder(); body.subscribe(buffer -> { byte[] bytes = new byte[buffer.readableByteCount()]; buffer.read(bytes); // DataBufferUtils.release(buffer); String bodyString = new String(bytes, StandardCharsets.UTF_8); sb.append(bodyString); });returnformatStr(sb.toString()); } /** * remove Spaces, newlines, and tabs * @param STR * @return
     */
    private static String formatStr(String str){
        if(str ! = null && str.length() > 0) { Pattern p = Pattern.compile("\\s*|\t|\r|\n");
            Matcher m = p.matcher(str);
            return m.replaceAll("");
        }
        returnstr; }}Copy the code

In fact, in the process of searching information online, I found that there are two ways to parse body content widely mentioned online. One is the above way, which reads bytes and splices strings, and the other is as follows:

private String getBodyContent(ServerWebExchange exchange){ Flux<DataBuffer> body = exchange.getRequest().getBody(); AtomicReference<String> bodyRef = new AtomicReference<>(); Body. Subscribe (dataBuffer -> {CharBuffer CharBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer()); DataBufferUtils.release(dataBuffer); bodyRef.set(charBuffer.toString()); }); // Get the Request bodyreturn bodyRef.get();
    }
Copy the code

However, some Internet users said that this method can obtain up to 1024 bytes of data, too long data will be truncated, resulting in data loss. Here the author has not personally verified, just provide this way here for everyone’s reference. Also note that after we create the ByteBuf object, its reference count is 1. When you release the reference count object every time you call databufferUtils.release, its reference count is reduced by 1. If the reference count is 0, The reference count object is deallocate and the object pool is returned. When trying to access the reference count to zero reference counting objects will be thrown IllegalReferenceCountException anomalies are as follows:

io.netty.util.IllegalReferenceCountException: refCnt: 0 at io.netty.buffer.AbstractByteBuf.ensureAccessible(AbstractByteBuf.java:1423) ~ [netty - all - 4.1.0. Final. Jar: 4.1.0. The Final] at io.net ty. Buffer. UnpooledHeapByteBuf. Capacity (UnpooledHeapByteBuf. Java: 102) ~ [netty - all - 4.1.0. Final. Jar: 4.1.0. The Final] at io.net ty. Buffer. ReadOnlyByteBuf. Capacity (ReadOnlyByteBuf. Java: 408) ~ [netty - all - 4.1.0. Final. Jar: 4.1.0. The Final] at io.net ty. Buffer. AbstractByteBuf. SetIndex (AbstractByteBuf. Java: 126) ~ [netty - all - 4.1.0. Final. Jar: 4.1.0. The Final] at io.net ty. Buffer. ReadOnlyByteBuf. < init > (50) ReadOnlyByteBuf. Java: ~ [netty - all - 4.1.0. Final. Jar: 4.1.0. The Final] at io.net ty. Buffer. ReadOnlyByteBuf. Duplicate (ReadOnlyByteBuf. Java: 278) ~ [netty - all - 4.1.0. Final. Jar: 4.1.0. The Final]Copy the code

So in order to get the body data using the same method across multiple custom filters, we don’t release it.

Refer to the article

  • Blog.csdn.net/hong10086/a…
  • My.oschina.net/LucasZhu/bl…
  • Blog.51cto.com/thinklili/2…