一、概述
背景:使用
Spring Cloud Gateway
默认过滤器,无法捕获里面返回的状态,从而实现自定义返回体。
举个栗子:
在使用 gateway
过滤器 RequestRateLimiter
时候,当请求被限制时候,返回请求 Response
的 HTTP
的状态码 429
。
但这时候,根据业务约定,需要返回自定义的 “返回体”,如下:
{
"code": 11429,
"msg": "request limit"
}
那么问题来了:如果捕获这个 Response
,修改返回体呢?
二、预备知识
先来了解下 gateway
基础知识,问自己两个问题:
Filter
调用顺序是怎样的?Pre
和Post
的Filter
如何定义的?
基础知识嘛,那就看官方文档喽:基础知识嘛,那就看官方文档喽:官网地址
- 问题1,结合官网可知道:how-it-works
Filter
的Order
越小越排前- 对于
Pre Filter
,执行顺序同排序顺序- 对于
Post Filter
,执行顺序与排序顺序相反
如图:
- 问题2,结合官网可知道:custom-filter
Filter
没有明确表明Pre
和Post
,那么可以认为一个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
}
}
三、解决方法
解决方法:
- 直接修改返回体
- 重写
gateway
的过滤器
实验环境:
- 两个服务:
gateway
服务(端口 8882) 和consumer
服务( 端口 8081) - 版本,如下:
<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
。
实验步骤如下:
- 创建自定义
Filter
gateway
简单配置curl
请求访问
- 创建自定义
Filter
可以使用
gateway
默认的filter
:ModifyResponseBodyGatewayFilterFactory
只需要继承他,并重载方法。
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);
};
}
}
gateway
配置
server:
port: 8882
spring:
cloud:
gateway:
routes:
- id: limit_route
uri: http://127.0.0.1:8081
predicates:
- Path=/hello/**
filters:
- CustomResponseFilterFactory
- 访问:
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
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;
}
}
}
- 配置如下:
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