background

The technology stack used in the project is Spring Cloud, with the following functional requirements:

In business, the service of the Spring Cloud Gateway module can obtain the token and obtain the identity information from the token after the authentication is passed.

Now you want to populate the request parameters with the identity information (which encapsulates multiple data into a BaseDTO object for extension).

As for the subsequent microservice module processing specific business, as long as it inherits the BaseDTO object, it can directly obtain the identity information for business logic processing.

Problem description

In a nutshell, the problem is how Spring Cloud Gateway dynamically adds request parameters.

Spring Cloud Gateway Add Request Parameter

  1. Check the official documentation, which provides the following example:

Docs. Spring. IO/spring – clou…

But it’s written in the configuration file, so it looks like it has to be fixed.

  1. I saw a similar question on Github,

Github.com/spring-clou…

But the implementation is similar to a configuration file.

  1. Check a similar answer on StackOverflow:

Stackoverflow.com/questions/6…

I think I have a direction.

The solution

In the Spring Cloud Gateway source, found the two class AddRequestParameterGatewayFilterFactory and ModifyRequestBodyGatewayFilterFactory

The code content is as follows:

AddRequestParameterGatewayFilterFactory

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.cloud.gateway.filter.factory;

import java.net.URI;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractNameValueGatewayFilterFactory.NameValueConfig;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;

public class AddRequestParameterGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    public AddRequestParameterGatewayFilterFactory(a) {}public GatewayFilter apply(NameValueConfig config) {
        return (exchange, chain) -> {
            URI uri = exchange.getRequest().getURI();
            StringBuilder query = new StringBuilder();
            String originalQuery = uri.getRawQuery();
            if (StringUtils.hasText(originalQuery)) {
                query.append(originalQuery);
                if (originalQuery.charAt(originalQuery.length() - 1) != '&') {
                    query.append('&');
                }
            }

            query.append(config.getName());
            query.append('=');
            query.append(config.getValue());

            try {
                URI newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(query.toString()).build(true).toUri();
                ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
                return chain.filter(exchange.mutate().request(request).build());
            } catch (RuntimeException var8) {
                throw new IllegalStateException("Invalid URI query: \"" + query.toString() + "\" "); }}; }}Copy the code

ModifyRequestBodyGatewayFilterFactory

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.cloud.gateway.filter.factory.rewrite;

import java.util.Map;
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.cloud.gateway.support.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.support.DefaultServerRequest;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public class ModifyRequestBodyGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyRequestBodyGatewayFilterFactory.Config> {
    public ModifyRequestBodyGatewayFilterFactory(a) {
        super(ModifyRequestBodyGatewayFilterFactory.Config.class);
    }

    / * *@deprecated* /
    @Deprecated
    public ModifyRequestBodyGatewayFilterFactory(ServerCodecConfigurer codecConfigurer) {
        this(a); }public GatewayFilter apply(ModifyRequestBodyGatewayFilterFactory.Config config) {
        return (exchange, chain) -> {
            Class inClass = config.getInClass();
            ServerRequest serverRequest = newDefaultServerRequest(exchange); Mono<? > modifiedBody = serverRequest.bodyToMono(inClass).flatMap((o) -> {return config.rewriteFunction.apply(exchange, o);
            });
            BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, config.getOutClass());
            CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getRequest().getHeaders());
            return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
                ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                    public HttpHeaders getHeaders(a) {
                        HttpHeaders httpHeaders = new HttpHeaders();
                        httpHeaders.putAll(super.getHeaders());
                        httpHeaders.set("Transfer-Encoding"."chunked");
                        return httpHeaders;
                    }

                    public Flux<DataBuffer> getBody(a) {
                        returnoutputMessage.getBody(); }};return chain.filter(exchange.mutate().request(decorator).build());
            }));
        };
    }

    public static class Config {
        private Class inClass;
        private Class outClass;
        private Map<String, Object> inHints;
        private Map<String, Object> outHints;
        private RewriteFunction rewriteFunction;

        public Config(a) {}public Class getInClass(a) {
            return this.inClass;
        }

        public ModifyRequestBodyGatewayFilterFactory.Config setInClass(Class inClass) {
            this.inClass = inClass;
            return this;
        }

        public Class getOutClass(a) {
            return this.outClass;
        }

        public ModifyRequestBodyGatewayFilterFactory.Config setOutClass(Class outClass) {
            this.outClass = outClass;
            return this;
        }

        public Map<String, Object> getInHints(a) {
            return this.inHints;
        }

        public ModifyRequestBodyGatewayFilterFactory.Config setInHints(Map<String, Object> inHints) {
            this.inHints = inHints;
            return this;
        }

        public Map<String, Object> getOutHints(a) {
            return this.outHints;
        }

        public ModifyRequestBodyGatewayFilterFactory.Config setOutHints(Map<String, Object> outHints) {
            this.outHints = outHints;
            return this;
        }

        public RewriteFunction getRewriteFunction(a) {
            return this.rewriteFunction;
        }

        public <T, R> ModifyRequestBodyGatewayFilterFactory.Config setRewriteFunction(Class<T> inClass, Class<R> outClass, RewriteFunction<T, R> rewriteFunction) {
            this.setInClass(inClass);
            this.setOutClass(outClass);
            this.setRewriteFunction(rewriteFunction);
            return this;
        }

        public ModifyRequestBodyGatewayFilterFactory.Config setRewriteFunction(RewriteFunction rewriteFunction) {
            this.rewriteFunction = rewriteFunction;
            return this; }}}Copy the code

In fact, it can be used as an official reference example.

We can follow the example of adding parameters to our own gateway filter.

The implementation code

Process authentication filters

@Component
public class AuthFilter implements GlobalFilter.Ordered {

    private static final Logger LOGGER = LoggerFactory.getLogger(AuthFilter.class);

    private static AntPathMatcher antPathMatcher;

    static {
        antPathMatcher = new AntPathMatcher();
    }

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

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

        // Skip paths that do not require validation
        Stream<String> skipAuthUrls = UrlConstant.skipAuthUrls.stream();
        if(skipAuthUrls.anyMatch(path -> antPathMatcher.match(path, url))){
            // Return directly
            ServerHttpRequest.Builder builder = request.mutate();
			return chain.filter(exchange.mutate().request(builder.build()).build());
        }
        // Retrieve the token from the request header
        String token = request.getHeaders().getFirst("Authorization");

        // Retrieve the identity information contained in the token
        // The token logic is not explained
        BaseDTO baseDTO = getClaim(token);
        if(null == baseDTO){
	    // If the authentication fails, the identity information cannot be obtained
            return illegalResponse(exchange, "{\"code\": \"401\",\"msg\": \"unauthorized.\"}");
        }


        // Add the current identity to the current request
        ServerHttpRequest.Builder builder = request.mutate();

        Stream<String> addRequestParameterUrls = UrlConstant.addRequestParameterUrls.stream();
        if (addRequestParameterUrls.anyMatch(path -> antPathMatcher.match(path, url))){
            // Request parameters need to be added
            if(request.getMethod() == HttpMethod.GET){
                // get request processing parameters
                return addParameterForGetMethod(exchange, chain, uri, baseDTO, builder);
            }

            if(request.getMethod() == HttpMethod.POST){
                // Post request processing parameters
                MediaType contentType = request.getHeaders().getContentType();
                if(MediaType.APPLICATION_JSON.equals(contentType)
                        || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){
                    // Request content is application json
                    return addParameterForPostMethod(exchange, chain, baseDTO);
                }

                if (MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType)) {
                    // Request form data
                    returnaddParameterForFormData(exchange, chain, baseDTO, builder); }}if(request.getMethod() == HttpMethod.PUT){
                // put request processing parameters
                // Go through the POST request process
                return addParameterForPostMethod(exchange, chain, baseDTO);
            }

            if(request.getMethod() == HttpMethod.DELETE){
                // delete request processing parameters
                // Go through the get request process
                returnaddParameterForGetMethod(exchange, chain, uri, baseDTO, builder); }}// Current filter No further action is required
        returnchain.filter(exchange.mutate().request(builder.build()).build()); }}Copy the code

Get request adds parameters

/** * get request, add parameter *@param exchange
     * @param chain
     * @param uri
     * @param baseDTO
     * @param builder
     * @return* /
    private Mono<Void> addParameterForGetMethod(ServerWebExchange exchange, GatewayFilterChain chain, URI uri, BaseDTO baseDTO, ServerHttpRequest.Builder builder) {
        StringBuilder query = new StringBuilder();

        String originalQuery = uri.getQuery();
        if (StringUtils.hasText(originalQuery)) {
            query.append(originalQuery);
            if (originalQuery.charAt(originalQuery.length() - 1) != '&') {
                query.append('&');
            }
        }

        query.append("userId").append("=").append(baseDTO.getUserId())
                .append("&").append("userName").append("=").append(baseDTO.getUserName())
        ;

        try {
            URI newUri = UriComponentsBuilder.fromUri(uri).replaceQuery(query.toString()).build().encode().toUri();
            ServerHttpRequest request = exchange.getRequest().mutate().uri(newUri).build();
            return chain.filter(exchange.mutate().request(request).build());
        } catch (Exception e) {
            LOGGER.error("Invalid URI query: " + query.toString(), e);
            // Current filter No further action is required
            returnchain.filter(exchange.mutate().request(builder.build()).build()); }}Copy the code

Post requests to add parameters

The request content is Application JSON

/** * Post request, add parameter *@param exchange
     * @param chain
     * @param baseDTO
     * @return* /
    private Mono<Void> addParameterForPostMethod(ServerWebExchange exchange, GatewayFilterChain chain, BaseDTO baseDTO) {
        ServerRequest serverRequest = new DefaultServerRequest(exchange);
        AtomicBoolean flag = new AtomicBoolean(false);
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap((o) -> {
            if(o.startsWith("[")) {// The body content is an array and is returned directly
                return Mono.just(o);
            }

            ObjectMapper objectMapper = new ObjectMapper();
            try {
                Map map = objectMapper.readValue(o, Map.class);

                map.put("userId", baseDTO.getUserId());
                map.put("userName", baseDTO.getUserName());

                String json = objectMapper.writeValueAsString(map);
                LOGGER.info("addParameterForPostMethod -> json = {}", json);
                return Mono.just(json);
            }catch (Exception e){
                e.printStackTrace();
                returnMono.just(o); }}); BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class); CachedBodyOutputMessage outputMessage =new CachedBodyOutputMessage(exchange, exchange.getRequest().getHeaders());
        return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
            ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                public HttpHeaders getHeaders(a) {
                    HttpHeaders httpHeaders = new HttpHeaders();
                    httpHeaders.putAll(super.getHeaders());
                    httpHeaders.set("Transfer-Encoding"."chunked");
                    return httpHeaders;
                }

                public Flux<DataBuffer> getBody(a) {
                    returnoutputMessage.getBody(); }};return chain.filter(exchange.mutate().request(decorator).build());
        }));
    }
Copy the code

Request content is form Data

/** * add parameter * to form data for post request@param exchange
     * @param chain
     * @param baseDTO
     * @param builder
     * @return* /
    private Mono<Void> addParameterForFormData(ServerWebExchange exchange, GatewayFilterChain chain, BaseDTO baseDTO, ServerHttpRequest.Builder builder) {
        builder.header("userId", String.valueOf(baseDTO.getUserId()));
        try {
            builder.header("userName", URLEncoder.encode(String.valueOf(baseDTO.getUserName()), "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            builder.header("userName", String.valueOf(baseDTO.getUserName()));
        }
        ServerHttpRequest serverHttpRequest = builder.build();
        HttpHeaders headers = serverHttpRequest.getHeaders();

        return chain.filter(exchange.mutate().request(serverHttpRequest).build());
    }
Copy the code

Return data processing

/** * return message *@param exchange
     * @param data
     * @return* /
    private Mono<Void> illegalResponse(ServerWebExchange exchange, String data) {
        ServerHttpResponse originalResponse = exchange.getResponse();
        originalResponse.setStatusCode(HttpStatus.OK);
        originalResponse.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE);
        byte[] response = data.getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
        return originalResponse.writeWith(Flux.just(buffer));
    }
Copy the code

The final result

As described above, the userId and userName attributes are written to the request parameter.

In the service module of specific business processing, the parameter of the Controller layer can be obtained as long as it inherits the BaseDTO class containing the two attributes of userId and userName to be used in the actual business process.