Predict决定了请求由哪一个路由处理,在路由处理之前,需要经过“pre”类型的过滤器处理,处理返回响应之后,可以由“post”类型的过滤器处理。在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。
如下图所示,客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,比如上图中的user-service,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端。
Spring Cloud Gateway的Filter分为两种:GatewayFilter 与 GlobalFilter。GlobalFilter会应用到所有的路由上,而GatewayFilter将应用到单个路由或者一个分组的路由上。
Gateway filter
GatewayFilter工厂可以在配置文件application.yml中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名。在配置文件中配置的GatewayFilter Factory最终都会相应的过滤器工厂类处理。
Spring Cloud Gateway包含许多内置的GatewayFilter工厂,如下图所示:
下面讲述几个常见的过滤器工厂:
1 AddRequestHeader GatewayFilter Factory
1.1 yml配置
server:
port: 8080
spring:
application:
name: test-GATEWAY
cloud:
gateway:
routes:
- id: test-route
uri: http://httpbin.org:80/get
predicates:
- After=2020-01-09T00:00:00+08:00[Asia/Shanghai]
filters:
- AddRequestHeader=Test,gateway
1.2 分析并查看结果
在上述配置中,配置了router的id为test-route,路由地址为http://httpbin.org:80/get,该router有AfterPredictFactory,有一个filter为AddRequestHeaderGatewayFilterFactory(约定写成AddRequestHeader),AddRequestHeader过滤器工厂会在请求头加上一对请求头,名称为Test,值为gateway。我们在浏览器中输入:http://localhost:8080/get,得到如下:
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,de;q=0.7",
"Cache-Control": "max-age=0",
"Cookie": "Hm_lvt_510ed80f0be980f190b7bfc3a27fabd9=1556184289; Hm_lvt_3242ee7ea9b8b5d0e409bb74bc4e0091=1556184289; Phpstorm-d6b3462e=aa37f591-2c2c-49d9-9293-479590c634c0; Idea-8bd2b531=a445ac7e-83b1-496c-baaa-96bc8d5cee14",
"Forwarded": "proto=http;host=\"localhost:8080\";for=\"0:0:0:0:0:0:0:1:56947\"",
"Host": "httpbin.org",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Test": "gateway",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36",
"X-Forwarded-Host": "localhost:8080"
},
"origin": "0:0:0:0:0:0:0:1, 139.199.106.7, ::1",
"url": "https://localhost:8080/get"
}
在上述响应结果中,确实在请求头中加入了Test这样的一个请求头,在配置文件中配置的AddRequestHeader过滤器工厂生效。
2 RewritePath GatewayFilter Factory
2.1 yml配置
server:
port: 8080
spring:
application:
name: test-GATEWAY
cloud:
gateway:
routes:
- id: test-route
uri: https://blog.csdn.net/
predicates:
- Path=/sso/**
filters:
- RewritePath=/sso/(?<segment>.*),/$\{segment}
2.2 分析并查看结果
上面的配置中,所有的/sso/**开始的路径都会命中配置的router,并执行过滤器的逻辑,在本案例中配置了RewritePath过滤器工厂,此工厂将/sso/(?.*)重写为{segment},然后转发到https://blog.csdn.net。比如在网页上请求localhost:8080/sso/mameng1988,此时会将请求转发到https://blog.csdn.net/mameng1988的页面。
自定义过滤器
Spring Cloud Gateway内置了19种过滤器工厂,可以满足大部分场景,但我们也可以自定义过滤器。在Spring Cloud Gateway中,我们实现GatewayFilter和Ordered2个接口就可以自定义过滤器了,下面我们写一个
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author by mazhen
* @Classname CustomerFilter
* @Description TODO
* @Date 2020/1/11 22:22
*/
@Component
public class CustomerFilter implements GatewayFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(CustomerFilter.class);
private static final String REQUEST_START_TIME = "Start_time";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(REQUEST_START_TIME,System.currentTimeMillis());
return chain.filter(exchange).then(
Mono.fromRunnable(
() -> {
Long startTime = exchange.getAttribute(REQUEST_START_TIME);
if (null != startTime) {
logger.info(exchange.getRequest().getURI().getRawPath() + ": "
+ (System.currentTimeMillis() - startTime) + "ms");
}
}
)
);
}
@Override
public int getOrder() {
return 0;
}
}
在上面的代码中,Ordered中的getOrder()方法是来给过滤器设定优先级的,值越大则优先级越低。还有一个filter(exchange,chain)方法,在该方法中,先记录了请求的开始时间,并保存在ServerWebExchange中,此处是一个“pre”类型的过滤器,然后再chain.filter的内部类中的run()方法中相当于"post"过滤器,在此处打印了请求所消耗的时间。然后将该过滤器注册到router中,代码如下:
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
// @formatter:off
return builder.routes()
.route(r -> r.path("/sso/**")
.filters(f -> f.filter(new CustomerFilter()))
.uri("lb://test-SSO")
.order(0)
.id("customer_filter_router")
)
.build();
// @formatter:on
}
重启服务,通过浏览器输入:http://localhost:8080/sso/test
在服务的控制台输出一下的请求信息的日志:
[20200111 22:53:52][INFO ][appKey=IAM-GATEWAY][traceId=][SpanId=] c.c.i.g.c.CustomerFilter [reactor-http-nio-4]
[CustomerFilter ][32 ] - /sso/test: 2514ms
由于自定义过滤器无法在配置文件中配置,而自定义过滤器工厂是可以的,下面我们实现一个过滤器工厂。
自定义过滤器工厂
在实现自定义过滤器工厂之前,我们先看一下GatewayFilterFactory的源码,其层级如下:
过滤器工厂的顶级接口是GatewayFilterFactory,有两个较接近具体实现的抽象类,分别为AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这两个类前者接收一个参数,比如它的实现类RedirectToGatewayFilterFactory;后者接收两个参数,比如它的实现类AddRequestHeaderGatewayFilterFactory类。现在需要将请求的日志打印出来,需要使用一个参数,这时可以参照RedirectToGatewayFilterFactory的写法。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.List;
/**
* @author by mazhen
* @Classname CustomerGatewayFilterFactory
* @Description 自定义过滤器工厂
* @Date 2020/1/11 23:17
*/
@Component
public class CustomerGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomerGatewayFilterFactory.Config> {
private static final Logger logger = LoggerFactory.getLogger(CustomerGatewayFilterFactory.class);
private static final String REQUEST_START_TIME = "startTime";
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("enabled");
}
public CustomerGatewayFilterFactory () {
super(Config.class);
logger.info("Loaded CustomerGatewayFilterFactory");
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
if (!config.isEnabled()) {
return chain.filter(exchange);
}
exchange.getAttributes().put(REQUEST_START_TIME,System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(REQUEST_START_TIME);
if (startTime != null) {
StringBuilder sb = new StringBuilder(exchange.getRequest().getURI().getRawPath())
.append(": ")
.append(System.currentTimeMillis() - startTime)
.append("ms");
sb.append(" params:").append(exchange.getRequest().getQueryParams());
sb.append(" heads:").append(exchange.getRequest().getHeaders());
logger.info(sb.toString());
}
}));
};
}
public static class Config {
/**
* 控制是否开启统计
*/
private boolean enabled;
public Config() {}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}
配置文件
server:
port: 8080
spring:
application:
name: test-GATEWAY
cloud:
gateway:
routes:
- id: sso-route
uri: lb://test-sso
predicates:
- Path=/sso/**
filters:
- Customer=true
上面的配置文件中为什么是Customer属性,那是有个规范,就是工厂类要以GatewayFilterFactory为结尾,前面的名称为配置文件的属性,所以配置文件中就用Customer作为属性。值true赋值给的对象就CustomerGatewayFilterFactory中的config内部类,设置true就是对enable进行赋值。
重启服务后,和上面的自定义过滤器的访问效果是一样的。
GlobalFilter
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:
- GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters配置在全局,作用在所有路由上
- GlobalFilter : 全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。
Spring Cloud Gateway框架内置的GlobalFilter如下:
上图中每一个GlobalFilter都作用在每一个router上,能够满足大多数的需求。但是如果遇到业务上的定制,可能需要编写满足自己需求的GlobalFilter。在下面的案例中将讲述如何编写自己GlobalFilter,该GlobalFilter会校验请求中是否包含了请求参数“token”,如何不包含请求参数“token”则不转发路由,否则执行正常的逻辑。代码如下:
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author by mazhen
* @Classname AuthorizeFilter
* @Description TODO
* @Date 2020/1/12 00:17
*/
@Component
public class AuthorizeFilter implements GlobalFilter , Ordered {
private static final Logger logger = LoggerFactory.getLogger(AuthorizeFilter.class);
private static final String AUTHORIZE_TOKEN = "token";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst(AUTHORIZE_TOKEN);
if (StringUtils.isBlank(token)) {
logger.info("token is empty");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
启动请求
以上请求没有参数token,无法转发路由。看看下面我们带上参数token
参考文章:微服务网关Gateway在实战中如何结合注册中心,定义过滤器?
spring cloud gateway之filter篇