background

There is a business requirement to provide a set of apis for third parties to call.

Before dealing with the specific business interface, a simple authentication should be done in the design. After the identity transmission parameters are negotiated, the identity verification is done in the Gateway module, considering that the Spring Cloud Gateway has been used in the project.

Therefore, when the server obtains a request, it must intercept and obtain the request transmission parameters before performing subsequent authentication logic.

Here’s a problem to solve: How does the Spring Cloud Gateway read the request pass parameter?

Spring Cloud Gateway get Request Body

Problem description

Problem: Spring Cloud Gateway reads request parameters

Only two cases are dealt with briefly here, GET and POST requests.

If a get request is found, the parameter on the URL is taken; If a POST request is found, the contents of the body are read.

The solution

Reference github.com/spring-clou…

Two filters are defined. The first filter, ApiRequestFilter, takes the parameters and places them in the GatewayContext.

Note If it is a POST request, after the request body is read, it must be reconstructed and filled back into the request body.

The second filter, ApiVerifyFilter, gets the parameters directly from the context.

Later, if other services also need to read parameters, they can directly obtain the parameters from the context, without repeating the logic of obtaining parameters.

The implementation code

GatewayContext

@Data
public class GatewayContext {
    public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext";

    /** * cache json body */
    private String cacheBody;
    /** * cache form data */
    private MultiValueMap<String, Part> formData;
    /** * cache request path */
    private String path;
}
Copy the code

ApiRequestFilter


@Component
@Slf4j
public class ApiRequestFilter implements GlobalFilter.Ordered {

    private static AntPathMatcher antPathMatcher;

    static {
        antPathMatcher = new AntPathMatcher();
    }

    /** * default HttpMessageReader */
    private static finalList<HttpMessageReader<? >> messageReaders = HandlerStrategies.withDefaults().messageReaders();private static final ResolvableType MULTIPART_DATA_TYPE = ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, Part.class);

    private static final Mono<MultiValueMap<String, Part>> EMPTY_MULTIPART_DATA = Mono.just(CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<String, Part>(0))).cache();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String url = request.getURI().getPath();

        if(request.getMethod() == HttpMethod.GET){
            // get request processing parameters
            return handleGetMethod(exchange, chain, request);
        }

        if(request.getMethod() == HttpMethod.POST){
            // Post request processing parameters
            return handlePostMethod(exchange, chain, request);
        }

        return chain.filter(exchange);
    }

    /** * get request processing parameters *@param exchange
     * @param chain
     * @param request
     * @return* /
    private Mono<Void> handleGetMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
        // TODO does not do processing temporarily

        return chain.filter(exchange);
    }

    /** * Post request verification parameter *@param exchange
     * @param chain
     * @param request
     * @return* /
    private Mono<Void> handlePostMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request){
        GatewayContext gatewayContext = new GatewayContext();
        gatewayContext.setPath(request.getPath().pathWithinApplication().value());
        /** * save gateway context into exchange */
        exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT, gatewayContext);

        MediaType contentType = request.getHeaders().getContentType();
        if(MediaType.APPLICATION_JSON.equals(contentType)
                || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){
            // Request content is application json

            // Rebuild the request body
            return readJsonBody(exchange, chain, gatewayContext);
        }

        if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
            // Request form data
            return readFormData(exchange, chain, gatewayContext);
        }
        return chain.filter(exchange);
    }

    /** * POST request * restructures the request body *@param exchange
     * @param chain
     * @param gatewayContext
     * @return* /
    private Mono<Void> readJsonBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) {
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .flatMap(dataBuffer -> {
                    /* * read the body Flux
      
       , 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);
                    });
                    /** * repackage ServerHttpRequest */
                    ServerHttpRequest mutatedRequest =
                            new ServerHttpRequestDecorator(exchange.getRequest()) {
                                @Override
                                public Flux<DataBuffer> getBody(a) {
                                    returncachedFlux; }};/** * mutate exchage with new ServerHttpRequest */
                    ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
                    /** * read body string with default messageReaders */
                    return ServerRequest.create(mutatedExchange, messageReaders)
                            .bodyToMono(String.class)
                            .doOnNext(objectValue -> {
                                // save body into gatewayContext
                                gatewayContext.setCacheBody(objectValue);
                            })
                            .then(chain.filter(mutatedExchange));
                });
    }

    private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext) {
        return exchange.getRequest().getBody().collectList().flatMap(dataBuffers -> {
            final byte[] totalBytes = dataBuffers.stream().map(dataBuffer -> {
                try {
                    final byte[] bytes = IOUtils.toByteArray(dataBuffer.asInputStream());
// System.out.println(new String(bytes));
                    return bytes;
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }).reduce(this::addBytes).get();
            final ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody(a) {
                    returnFlux.just(buffer(totalBytes)); }};final ServerCodecConfigurer configurer = ServerCodecConfigurer.create();
            final Mono<MultiValueMap<String, Part>> multiValueMapMono = repackageMultipartData(decorator, configurer);
            return multiValueMapMono.flatMap(part -> {
                for (String key : part.keySet()) {
                    // Enter the next loop if it is a file
                    if (key.equals("file")) {
                        continue;
                    }
                    part.getFirst(key).content().subscribe(buffer -> {
                        final byte[] bytes = new byte[buffer.readableByteCount()];
                        buffer.read(bytes);
                        DataBufferUtils.release(buffer);
                        try {
                            final String bodyString = new String(bytes, "utf-8");
                            gatewayContext.setCacheBody(bodyString);
                        } catch(UnsupportedEncodingException e) { e.printStackTrace(); }}); }return chain.filter(exchange.mutate().request(decorator).build());
            });
        });
    }

    @SuppressWarnings("unchecked")
    private static Mono<MultiValueMap<String, Part>> repackageMultipartData(ServerHttpRequest request, ServerCodecConfigurer configurer) {
        try {
            final MediaType contentType = request.getHeaders().getContentType();
            if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
                return ((HttpMessageReader<MultiValueMap<String, Part>>) configurer.getReaders().stream().filter(reader -> reader.canRead(MULTIPART_DATA_TYPE, MediaType.MULTIPART_FORM_DATA))
                        .findFirst().orElseThrow(() -> new IllegalStateException("No multipart HttpMessageReader."))).readMono(MULTIPART_DATA_TYPE, request, Collections.emptyMap()) .switchIfEmpty(EMPTY_MULTIPART_DATA).cache(); }}catch (InvalidMediaTypeException ex) {
            // Ignore
        }
        return EMPTY_MULTIPART_DATA;
    }

    /**
     * addBytes.
     * @param first first
     * @param second second
     * @return byte
     */
    public byte[] addBytes(byte[] first, byte[] second) {
        final byte[] result = Arrays.copyOf(first, first.length + second.length);
        System.arraycopy(second, 0, result, first.length, second.length);
        return result;
    }

    private DataBuffer buffer(byte[] bytes) {
        final NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
        final DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
        buffer.write(bytes);
        return buffer;
    }


    @Override
    public int getOrder(a) {
        return FilterOrderConstant.getOrder(this.getClass().getName()); }}Copy the code

ApiVerifyFilter

@Component
@Slf4j
public class ApiVerifyFilter implements GlobalFilter.Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String url = request.getURI().getPath();
		
        if(request.getMethod() == HttpMethod.GET){
            // get request verification parameters
            return verifyGetMethod(exchange, chain, request);
        }

        if(request.getMethod() == HttpMethod.POST){
            // Post requests validation parameters
            return verifyPostMethod(exchange, chain, request);
        }
			
        return chain.filter(exchange);
    }

    /** * get Request validation parameter *@param exchange
     * @param chain
     * @param request
     * @return* /
    private Mono<Void> verifyGetMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
	// get requests to obtain parameters
        Map<String, String> queryParamMap = request.getQueryParams().toSingleValueMap();
		
	// Specific service parameters
        String secretId = queryParamMap.get("secretId");
        String secretKey = queryParamMap.get("secretKey");

	// Verify parameter logic
        return verifyParams(exchange, chain, secretId, secretKey);
    }

    /** * Post request verification parameter *@param exchange
     * @param chain
     * @param request
     * @return* /
    private Mono<Void> verifyPostMethod(ServerWebExchange exchange, GatewayFilterChain chain, ServerHttpRequest request) {
        try {
            GatewayContext gatewayContext = (GatewayContext)exchange.getAttributes().get(GatewayContext.CACHE_GATEWAY_CONTEXT);
            // get body from gatewayContext
            String cacheBody = gatewayContext.getCacheBody();

            Map map = new ObjectMapper().readValue(cacheBody, Map.class);

	    // Specific service parameters
            String secretId = String.valueOf(map.get("secretId"));
            String secretKey = String.valueOf(map.get("secretKey"));
           
	    // Verify parameter logic
            return verifyParams(exchange, chain, secretId, secretKey);

        } catch (Exception e){
            log.error("Failed to parse body content: {}", e);
            / / 403
            returnresponse(exchange, R.fail().enumCode(HttpCode.FORBIDDEN)); }}/** * Check parameter *@param exchange
     * @param chain
     * @param secretId
     * @param secretKey
     * @return* /
    private Mono<Void> verifyParams(ServerWebExchange exchange, GatewayFilterChain chain, String secretId, String secretKey) {
        // If the verification fails, a message is displayed
	// return response(exchange, R.fail().enumCode(HttpCode.UNAUTHORIZED));
        // todo

	// If the verification succeeds, the current filter is complete
        return chain.filter(exchange);
    }

    /** * response returns code *@param exchange
     * @param r
     * @return* /
    private Mono<Void> response(ServerWebExchange exchange, R r) {
        ServerHttpResponse originalResponse = exchange.getResponse();
        originalResponse.setStatusCode(HttpStatus.OK);
        originalResponse.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);

        try {
            byte[] bytes = new ObjectMapper().writeValueAsBytes(r);
            DataBuffer buffer = originalResponse.bufferFactory().wrap(bytes);
            return originalResponse.writeWith(Flux.just(buffer));
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null; }}@Override
    public int getOrder(a) {
        return FilterOrderConstant.getOrder(this.getClass().getName()); }}Copy the code