[SpringCloud Gateway] 之 修改 Response 请求体

6,496 阅读4分钟

一、概述

背景:使用 Spring Cloud Gateway 默认过滤器,无法捕获里面返回的状态,从而实现自定义返回体。


举个栗子:

在使用 gateway 过滤器 RequestRateLimiter时候,当请求被限制时候,返回请求 ResponseHTTP 的状态码 429。 但这时候,根据业务约定,需要返回自定义的 “返回体”,如下:

{
"code": 11429,
"msg": "request limit"
}

那么问题来了:如果捕获这个 Response,修改返回体呢?



二、预备知识

先来了解下 gateway 基础知识,问自己两个问题:

  1. Filter 调用顺序是怎样的?
  2. PrePostFilter 如何定义的?

基础知识嘛,那就看官方文档喽:基础知识嘛,那就看官方文档喽:官网地址

  1. 问题1,结合官网可知道:how-it-works
  1. FilterOrder 越小越排前
  2. 对于 Pre Filter,执行顺序同排序顺序
  3. 对于Post Filter,执行顺序与排序顺序相反

如图:


  1. 问题2,结合官网可知道:custom-filter

Filter 没有明确表明 PrePost,那么可以认为一个 Filter 可同时具有这两个操作。

如下是官网的例子:

// PreGatewayFilterFactory.java
public class PreGatewayFilterFactory extends AbstractGatewayFilterFactory<PreGatewayFilterFactory.Config> {

	public PreGatewayFilterFactory() {
		super(Config.class);
	}

	@Override
	public GatewayFilter apply(Config config) {
		// grab configuration from Config object
		return (exchange, chain) -> {
            //If you want to build a "pre" filter you need to manipulate the
            //request before calling chain.filter
            ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
            //use builder to manipulate the request
            return chain.filter(exchange.mutate().request(request).build());
		};
	}

	public static class Config {
        //Put the configuration properties for your filter here
	}

}

// PostGatewayFilterFactory.java
public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory<PostGatewayFilterFactory.Config> {

	public PostGatewayFilterFactory() {
		super(Config.class);
	}

	@Override
	public GatewayFilter apply(Config config) {
		// grab configuration from Config object
		return (exchange, chain) -> {
			return chain.filter(exchange).then(Mono.fromRunnable(() -> {
				ServerHttpResponse response = exchange.getResponse();
				//Manipulate the response in some way
			}));
		};
	}

	public static class Config {
        //Put the configuration properties for your filter here
	}

}


三、解决方法

解决方法:

  1. 直接修改返回体
  2. 重写 gateway 的过滤器

实验环境:

  1. 两个服务:gateway 服务(端口 8882) 和 consumer服务( 端口 8081)
  2. 版本,如下:
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud-alibaba.version>2.2.0.RELEASE</spring-cloud-alibaba.version>
        <spring-cloud.version>Hoxton.RELEASE</spring-cloud.version>
    </properties>

实验调用链路,如图:


(1)直接修改返回体

简单的想法是直接捕获了这个请求返回:判断 HTTP 状态是否为 429

实验步骤如下:

  1. 创建自定义Filter
  2. gateway 简单配置
  3. curl 请求访问

  1. 创建自定义 Filter

可以使用 gateway 默认的filterModifyResponseBodyGatewayFilterFactory 只需要继承他,并重载方法。

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.rewrite.RewriteFunction;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
 * @author donald
 * @date 2021/02/24
 */
@Component
public class CustomResponseFilterFactory extends ModifyResponseBodyGatewayFilterFactory {
    @Override
    public GatewayFilter apply(Config config) {
        return new ModifyResponseGatewayFilter(this.getConfig());
    }

    private Config getConfig() {
        Config cf = new Config();
        cf.setRewriteFunction(byte[].class, byte[].class, getRewriteFunction());
        return cf;
    }

    /**
     * 重写 Response 返回体
     *
     * 如果 HTTP status 为 429, 则修改
     *
     * @return 重写方法
     */
    private RewriteFunction<byte[], byte[]> getRewriteFunction() {
        return (exchange, resp) -> {
            if (exchange.getResponse().getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
                // 设置 HTTP 状态为 500
                exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
                String data = "{\"code\":" + 11429 + ",\"msg\": \"" + "request limit" + "\"}";
                return Mono.just(data.getBytes());
            }
            return Mono.just(resp);
        };
    }
}
  1. gateway 配置
server:
  port: 8882
spring:
  cloud:
    gateway:
      routes:
        - id: limit_route
          uri: http://127.0.0.1:8081
          predicates:
            - Path=/hello/**
          filters:
            - CustomResponseFilterFactory
  1. 访问:
donald@donald-pro:~$ curl -i http://localhost:8882/hello/1
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Date: Thu, 25 Feb 2021 08:10:34 GMT
Content-Length: 37

{"code":11429,"msg": "request limit"}


(2)重写 gateway 的过滤器

这个最为直接暴力,把 gateway 的默认Filter,直接重写。

例如,把 RequestRateLimiter 重写为 CustomResponseFilterFactory

  1. CustomResponseFilterFactory
import com.flying.fish.formwork.util.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;
import java.util.Map;

@Slf4j
@Component
public class CustomRequestRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomRequestRateLimiterGatewayFilterFactory.Config> {

    public static final String KEY_RESOLVER_KEY = "keyResolver";

    private final RateLimiter defaultRateLimiter;
    private final KeyResolver defaultKeyResolver;

    public CustomRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, @Qualifier("requestIdKeyResolver") KeyResolver defaultKeyResolver) {
        super(Config.class);
        this.defaultRateLimiter = defaultRateLimiter;
        this.defaultKeyResolver = defaultKeyResolver;
    }

    public KeyResolver getDefaultKeyResolver() {
        return defaultKeyResolver;
    }

    public RateLimiter getDefaultRateLimiter() {
        return defaultRateLimiter;
    }

    @SuppressWarnings("unchecked")
    @Override
    public GatewayFilter apply(Config config) {
        KeyResolver resolver = (config.keyResolver == null) ? defaultKeyResolver : config.keyResolver;
        RateLimiter<Object> limiter = (config.rateLimiter == null) ? defaultRateLimiter : config.rateLimiter;
        return (exchange, chain) -> {
            Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
            return resolver.resolve(exchange).flatMap(key ->
                    // TODO: if key is empty?
                    limiter.isAllowed(route.getId(), key).flatMap(response -> {
                        for (Map.Entry<String, String> header : response.getHeaders().entrySet()) {
                            exchange.getResponse().getHeaders().add(header.getKey(), header.getValue());
                        }
                        if (response.isAllowed()) {
                            return chain.filter(exchange);
                        }
                        ServerHttpResponse httpResponse = exchange.getResponse();
                        httpResponse.setStatusCode(config.getStatusCode());
                        log.error("访问已限流,请稍候再请求");
                        String data = "{\"code\":" + 11429 + ",\"msg\": \"" + "request limit" + "\"}";
                        DataBuffer buffer = httpResponse.bufferFactory().wrap(data.getBytes(StandardCharsets.UTF_8));
                        return httpResponse.writeWith(Mono.just(buffer));
                    }));
        };
    }

    public static class Config {
        private KeyResolver keyResolver;
        private RateLimiter rateLimiter;
        private HttpStatus statusCode = HttpStatus.TOO_MANY_REQUESTS;

        public KeyResolver getKeyResolver() {
            return keyResolver;
        }

        public Config setKeyResolver(KeyResolver keyResolver) {
            this.keyResolver = keyResolver;
            return this;
        }
        public RateLimiter getRateLimiter() {
            return rateLimiter;
        }

        public Config setRateLimiter(RateLimiter rateLimiter) {
            this.rateLimiter = rateLimiter;
            return this;
        }

        public HttpStatus getStatusCode() {
            return statusCode;
        }

        public Config setStatusCode(HttpStatus statusCode) {
            this.statusCode = statusCode;
            return this;
        }
    }
}
  1. 配置如下:
server:
  port: 8882
spring:
  cloud:
    gateway:
      routes:
        - id: limit_route
          uri: http://127.0.0.1:8081
          predicates:
            - Path=/hello/**
          filters:
            - name: CustomRequestRateLimiter
              args:
                key-resolver: '#{@ideKeyResolver}' # 这个是自定义的,需自己实现哦。
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 3
  application:
    name: gateway-limiter
  redis:
    host: localhost
    port: 6379
    database: 0