body Verification or Modification of SpringCloud gateway request

  • 2021-11-01 03:20:54
  • OfStack

body Verification or Modification of SpringCloud gateway request

Subsequent versions have added the following filters


org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter

The following headers will be removed by default (the purpose of this is not known yet)

- connection

- keep-alive

- te

- transfer-encoding

- trailer

- proxy-authorization

- proxy-authenticate

- x-application-context

- upgrade

As a result, the transfer-encoding header that we added when we override the getHeaders method below is removed, resulting in the failure to parse body.

Solution:

Configure a custom header removal list in an yml file


spring:
  cloud:
      filter:
        remove-hop-by-hop:
          headers:
            - connection
            - keep-alive
            - te
            - trailer
            - proxy-authorization
            - proxy-authenticate
            - x-application-context
            - upgrade

Links are visible to the source code, and dynamic routing configuration can be realized: https://github.com/SingleTigger/SpringCloudGateway-Nacos-Demo

---Original----

Usually in business, we need to modify the request parameters in the gateway (note that the following is only for requests with body), and one is provided in springcloud and gateway

ModifyRequestBodyGatewayFilterFactory filter, looked at 1 under its implementation, need to specify the input type and output type, more limited.

I implemented an interceptor with reference to it myself

Note: The uploaded file also has a request body, which needs special treatment.

Here's the main code

 
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
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.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.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
 
/**
 * @author chenws
 * @date 2019/12/12 09:33:53
 */
@Component
public class CModifyRequestBodyGatewayFilterFactory extends AbstractGatewayFilterFactory { 
    private final List<HttpMessageReader<?>> messageReaders; 
    public CModifyRequestBodyGatewayFilterFactory() {
        this.messageReaders = HandlerStrategies.withDefaults().messageReaders();
    }
 
    @Override
    @SuppressWarnings("unchecked")
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerRequest serverRequest = ServerRequest.create(exchange,
                    this.messageReaders);
 
            Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                    .flatMap(originalBody -> modifyBody()
                            .apply(exchange,Mono.just(originalBody)));
 
            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody,
                    String.class);
            HttpHeaders headers = new HttpHeaders();
            headers.putAll(exchange.getRequest().getHeaders());
            headers.remove(HttpHeaders.CONTENT_LENGTH);
 
            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange,
                    headers);
            return bodyInserter.insert(outputMessage, new BodyInserterContext())
                    .then(Mono.defer(() -> {
                        ServerHttpRequest decorator = decorate(exchange, headers,
                                outputMessage);
                        return chain.filter(exchange.mutate().request(decorator).build());
                    })); 
        };
    }
 
    /**
     *  Modify body
     * @return apply  Return Mono<String> The data is modified body
     */
    private BiFunction<ServerWebExchange,Mono<String>,Mono<String>> modifyBody(){
        return (exchange,json)-> {
            AtomicReference<String> result = new AtomicReference<>();
            json.subscribe(
                    value -> {
                        //value  Is the request body , modified here 
                        result.set(value);
                        System.out.println(result.get());
                    },
                    Throwable::printStackTrace
            );
            return Mono.just(result.get());
        };
    }
 
    private ServerHttpRequestDecorator decorate(ServerWebExchange exchange, HttpHeaders headers,
                                        CachedBodyOutputMessage outputMessage) {
        return new ServerHttpRequestDecorator(exchange.getRequest()) {
            @Override
            public HttpHeaders getHeaders() {
                long contentLength = headers.getContentLength();
                HttpHeaders httpHeaders = new HttpHeaders();
                httpHeaders.putAll(super.getHeaders());
                if (contentLength > 0) {
                    httpHeaders.setContentLength(contentLength);
                }
                else {
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }
 
            @Override
            public Flux<DataBuffer> getBody() {
                return outputMessage.getBody();
            }
        };
    }
}

SpringCloud Gateway Get post Request Body (request body) Incomplete Solution

As a gateway service, Spring Cloud Gateway forwards requests through gateway. Before the request arrives at the back-end service, we can preprocess it through filter, such as the legitimacy of the request and the verification of merchants.

For example, we add merchant ID (merId) and merchant KEY (merkey) to the request body to verify the legitimacy of the request. But if the content of the request is too long, such as the file storage request converted to base64. At this point we get body content in filter will be truncated (too long Body will be truncated). At present, there is no good solution online.

springboot and Cloud versions are as follows;

版本
springboot 2.0.8.RELEASE
springcloud Finchley.SR2

Here is a solution, and the relevant code is as follows:

1.Requestfilter

We use the Gobalfilter of the Gateway Gateway to set up our first filter to filter all requests.

1) Through WebFlux of Spring 5, we use bodyToMono method to convert the response content into an String-like object, and the final result is Mono object

2). bodyToMono method We can get the full body content and return String.

3). We generate a 1-only token (via UUID) and put token into the requested header.

4). Store the obtained complete body content in redis.


@Component
public class RequestFilter implements GlobalFilter, Ordered {
	@Autowired
	private RedisClientTemplate redisClientTemplate;
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		DefaultServerRequest req = new DefaultServerRequest( exchange );
		String token = UUID.randomUUID().toString();
		// Toward headers Put in token Information 
		ServerHttpRequest serverHttpRequest =exchange.getRequest().mutate().header("token", token)
				.build();
		// Will the current request Become change Object 
		ServerWebExchange build = exchange.mutate().request( serverHttpRequest ).build();
		return req.bodyToMono( String.class ).map( str -> {
			redisClientTemplate.setObjex( "microservice:gateway:".concat( token ), 180, str );
			MySlf4j.textInfo( " Request parameter :{0}", str );
			return str;
		} ).then( chain.filter( build ) );
	}
	@Override
	public int getOrder() {
		return 0;
	}
}
2.MerchantAuthFilter

Establish a merchant authentication filter, and the relevant codes are as follows:

1). Get the token stored in the headers.

2). Get our body content stored in redis through token (blocking operations cannot be used in WebFlux, which is currently thought of).

3). After obtaining the complete body content, we can carry out the corresponding merchant authentication operation.

4). If the authentication is passed, the information will be rewritten, and if it is not passed, the abnormal information will be returned.


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
	/**  Verify that the merchant has permission to access  */
	ServerHttpRequest serverHttpRequest = exchange.getRequest();
	String token = serverHttpRequest.getHeaders().get( "token" ).get( 0 );
       String bodyStr = (String) redisClientTemplate.getObj("microservice:gateway:".concat(token));
	BaseReqVo baseReqVo = JsonUtil.fromJson( bodyStr, BaseReqVo.class );
	try {
		//  Merchant authentication 
		BaseRespVo<?> baseRespVo = merchantAuthService.checkMerchantAuth( baseReqVo );
		if (MicroserviceConstantParamUtils.RESULT_CODE_SUCC.equals( baseRespVo.getCode() )) {
               //  If the verification is successful, rewrite the information to avoid request After information consumption, the follow-up cannot be obtained from request The problem of obtaining information 
               URI uri = serverHttpRequest.getURI();
               ServerHttpRequest request = serverHttpRequest.mutate().uri(uri).build();
               DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
               Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
               request = new ServerHttpRequestDecorator(request) {
                   @Override
                   public Flux<DataBuffer> getBody() {
                       return bodyFlux;
                   }
               };
			//  Encapsulation request , pass it to the next 1 Grade 
               return chain.filter(exchange.mutate().request(request).build());
		} else {
			//  If the verification is unsuccessful, a prompt message will be returned 
			return gatewayResponse( baseRespVo.getCode(), baseRespVo.getMessage(), exchange );
		}
	} catch (MicroserviceServiceException ex) {
		//  If the verification is unsuccessful, a prompt message will be returned 
		MySlf4j.textError( " Merchant access authorization verification exception, exception code :{0}, Exception information :{1},  Anomaly {2}", ex.getCode(), ex.getMessage(), ex );
		return gatewayResponse( ex.getCode(), ex.getMessage(), exchange );
	} catch (Exception ex) {
		MySlf4j.textError( " Merchant Access Authentication Service Exception :{0}", LogUtil.ExceptionToString( ex ) );
		return gatewayResponse( MicroserviceException.ERR_100000, " System anomaly ", exchange );
	} finally {
		redisClientTemplate.del( "microservice:gateway:".concat( token ) );
	}
}
/** Data stream processing method */
private DataBuffer stringBuffer(String value) {
	byte[] bytes = value.getBytes( StandardCharsets.UTF_8 );
	NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory( ByteBufAllocator.DEFAULT );
	DataBuffer buffer = nettyDataBufferFactory.allocateBuffer( bytes.length );
	buffer.write( bytes );
	return buffer;
}
/** Gateway request response */
private Mono<Void> gatewayResponse(String code, String message, ServerWebExchange exchange) {
	//  If the verification is unsuccessful, a prompt message will be returned 
	ServerHttpResponse response = exchange.getResponse();
	BaseRespVo<T> baseRespVo = ResponseUtils.responseMsg( code, message, null );
	byte[] bits = JsonUtil.toJson( baseRespVo ).getBytes( StandardCharsets.UTF_8 );
	DataBuffer buffer = response.bufferFactory().wrap( bits );
	response.setStatusCode( HttpStatus.UNAUTHORIZED );
	//  Specify the code, otherwise it will be garbled in Chinese in the browser 
	response.getHeaders().add( "Content-Type", "text/plain;charset=UTF-8" );
	return response.writeWith( Mono.just( buffer ) );
}
@Override
public int getOrder() {
	return 1;
}

In addition, we can also implement request filtering and OAUTH authorization through GlobalFilter. The relevant codes are as follows:

Request Mode Validation Filter (RequestAuthFilter):


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 ServerHttpRequest serverHttpRequest = exchange.getRequest();
 String method = serverHttpRequest.getMethodValue();
 if (!"POST".equals(method)) {
  ServerHttpResponse response = exchange.getResponse();
  BaseRespVo<T> baseRespVo = ResponseUtils.responseMsg(MicroserviceException.ERR_100008, " Illegal request ", null);
  byte[] bits = JsonUtil.toJson(baseRespVo).getBytes(StandardCharsets.UTF_8);
  DataBuffer buffer = response.bufferFactory().wrap(bits);
  response.setStatusCode(HttpStatus.UNAUTHORIZED);
  // Specify the code, otherwise it will be garbled in Chinese in the browser 
  response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
  return response.writeWith(Mono.just(buffer));
 }
 return chain.filter(exchange);
 }

OAUTH Authorization Filter (OAuthSignatureFilter):


/** Authorized access user name */
@Value("${spring.security.user.name}")
private String securityUserName;
/** Authorized access password */
@Value("${spring.security.user.password}")
private String securityUserPassword;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 /**oauth Authorization */
 String auth = securityUserName.concat(":").concat(securityUserPassword);
 String encodedAuth = new sun.misc.BASE64Encoder().encode(auth.getBytes(Charset.forName("US-ASCII")));
 String authHeader = "Basic " + encodedAuth;
 // Toward headers Play authorization information in China 
 ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate().header("Authorization", authHeader)
   .build();
 // Will the current request Become change Object 
 ServerWebExchange build = exchange.mutate().request(serverHttpRequest).build();
 return chain.filter(build);
}

Related articles: