一、背景
- 都说学习一个东西带着一个问题去学效果才会更好,那么今天主要是什么问题呢,在这前我曾经总结过一个Bug,就是定时任务中用到的client因为request为空造成的空指针问题。当时解决办法是对client访问的接口所经过的网关专门配置一个不用经过校验的过滤器-KeyPairGatewayFilterFactory.
- 但是今天突然有一个疑问就是在网关的配置文件里面,指定的过滤器名为KeyPair,而不是KeyPairGatewayFilterFactory,那它是通过什么方法通过KeyPair映射到KeyPairGatewayFilterFactory这个过滤器的呢。
- 那么接下来就通过这样一个小问题去了解下gateway的过滤器以及过滤器name的映射
二、Gateway 过滤器总结
2.1、gateway各组件简介
- Zuul1.x 阻塞式IO 2.x 基于Netty,Spring Cloud GateWay天生就是异步非阻塞的,基于Reactor模型;
- 一个请求–>网关根据特定的条件匹配—>匹配成功之后可以将请求转发到指定的服务地址;在这个过程中,我们可以进行一些比较具体的控制(限流、日志、黑白名单)
- 路由(route):网关最基础的部分,也是网关比较基础的工作单元。路由由多个ID、多个多标URL(最终路由到的地址)、一系列的断言(匹配条件判断)和Filter过滤器(精细化控制)组成。如果断言为true,则匹配该路由。
- 断言(predicates):参考了Java8中的断言java.util.function.Predicate,开发人员可以匹配Http请求中的所有内容(包括请求头、请求参数等)(类似于nginx中的location匹配),如果断言与请求相匹配则路由。
- 过滤器(filter):一个标准的Spring webFilter,使用过滤器,可以在请求之前或者之后执行业务逻辑
- Predicates断言就是我们的匹配条件,Filter就可以理解为多个无所不能的拦截器,有了这两个元素,结合⽬标URL,就可以实现一个具体的路由转发。
2.2、filter作用和生命周期
2.2.1、作用
- 当我们有很多个服务时,比如下图中的user-service、goods-service、sales-service等服务,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。
- 对于这样重复的工作,有没有办法做的更好,答案是肯定的。在微服务的上一层加一个全局的权限控制、限流、日志输出的Api Gatewat服务,然后再将请求转发到具体的业务服务层。这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。
2.2.2、gateway生命周期
- 客户端向Spring Cloud GateWay发出请求,然后在GateWay Handler Mapping中找到与请求相匹配的路由,将其发送到GateWay Web Handler;Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(pre)或者之后(post)执行业务逻辑。
- Spring Cloud Gateway同zuul类似,有“pre”和“post”两种方式的filter。客户端的请求先经过“pre”类型的filter,然后将请求转发到具体的业务服务,比如上图中的user-service,收到业务服务的响应之后,再经过“post”类型的filter处理,最后返回响应到客户端。
- 与zuul不同的是,filter除了分为“pre”和“post”两种方式的filter外,在Spring Cloud Gateway中,filter从作用范围可分为另外两种,一种是针对于单个路由的gateway filter,它在配置文件中的写法同predict类似;另外一种是针对于所有路由的global gateway filer。现在从作用范围划分的维度来讲解这两种filter。
2.3、gatewayFilterFactory
- 过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。过滤器可以限定作用在某些特定请求路径上。 Spring Cloud Gateway包含许多内置的GatewayFilter工厂。
- GatewayFilter工厂同上一篇介绍的Predicate工厂类似,都是在配置文件application.yml中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名。在配置文件中配置的GatewayFilter Factory最终都会相应的过滤器工厂类处理。
- 那么就是这样的问题,让我们进入源码去看看为什么这样就可以呢?
我们可以去看一下过滤器工厂的name()方法(无论是自定义还是内置): - 它其实调用的是顶层接口GatewayFilterFactory的name():
- 可以看到它调用了NameUtils的normalizeFilterFactoryName(),参数是调用的类。
NameUtils是gateway为我们提供的工具类,点进该类的normalizeFilterFactoryName():
- 可以看到它内部调用了removeGarbage()方法,类似是给某个字符串移除“垃圾字符”的方法,那移除垃圾字符的方法可以不管,重点在于对哪个字符去移除垃圾,而这个字符一般就是我们最终返回的结果,也正是为什么过滤器工厂类在配置文件只需取真正过滤器名的原因。
clazz.getSimpleName()
.replace(GatewayFilterFactory.class.getSimpleName(), "")
getSimpleName()是jdk提供得到类的简写名称的方法。可以先理解为就是获得类名的方法,比如:
public static void main(String[] args) {
String simpleName = KeyPairGatewayFilterFactory.class.getSimpleName();
System.out.println(simpleName);
}
- 那么回到之前说的那个字符串,它的意思就是将调用类的简写名称中的GatewayFilterFactory这一部分替换为"",即空字符串,那么这就是为什么我们类叫xxGatewayFilterFactory,配置文件只需配置xx即可。
- 也是为什么自定义gatewayFilter工厂命名一定要是xxGatewayFilterFactory的原因。
- 然后回到知识点,Spring Cloud Gateway 内置的过滤器工厂一览表如下:
这里介绍几种经典的内置过滤器:
2.3.1、AddRequestHeaderGatewayFilterFactory
通过名称我们可以快速的明白这个过滤器工厂的作用,就是添加请求头。
使用示列:
server:
port: 8081
spring:
profiles:
active: add_request_header_route
---
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://httpbin.org:80/get
filters:
- AddRequestHeader=X-Request-Foo, Bar
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
profiles: add_request_header_route
- 在上述的配置中,工程的启动端口为8081,配置文件为add_request_header_route,在add_request_header_route配置中,配置了router的id为add_request_header_route,路由地址为http://httpbin.org:80/get,该router有AfterPredictFactory,有一个filter为AddRequestHeaderGatewayFilterFactory(约定写成AddRequestHeader),AddRequestHeader过滤器工厂会在请求头加上一对请求头,名称为X-Request-Foo,值为Bar。为了验证AddRequestHeaderGatewayFilterFactory是怎么样工作的,查看它的源码,AddRequestHeaderGatewayFilterFactory的源码如下:
public class AddRequestHeaderGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getName(), config.getValue())
.build();
return chain.filter(exchange.mutate().request(request).build());
};
}
}
- NameValueConfig就2个字段,一个name,一个value,对应的也就是请求头的名称和值。
- 由上面的代码可知,根据旧的ServerHttpRequest创建新的 ServerHttpRequest ,在新的ServerHttpRequest加了一个请求头,然后创建新的 ServerWebExchange ,提交过滤器链继续过滤。
- 启动工程,通过curl命令来模拟请求:
curl localhost:8081
最终显示了从 http://httpbin.org:80/get得到了请求,响应如下:
{
"args": {},
"headers": {
"Accept": "*/*",
"Connection": "close",
"Forwarded": "proto=http;host=\"localhost:8081\";for=\"0:0:0:0:0:0:0:1:56248\"",
"Host": "httpbin.org",
"User-Agent": "curl/7.58.0",
"X-Forwarded-Host": "localhost:8081",
"X-Request-Foo": "Bar"
},
"origin": "0:0:0:0:0:0:0:1, 210.22.21.66",
"url": "http://localhost:8081/get"
}
可以上面的响应可知,确实在请求头中加入了X-Request-Foo这样的一个请求头,在配置文件中配置的AddRequestHeader过滤器工厂生效。
- 如果我们需要传递多个请求头咋配置?
filters:
- AddRequestHeader=NAME, yinjihuan
- AddRequestHeader=NAME2, yinjihuan2
- 配置2个过滤器就行了,AddRequestHeaderGatewayFilterFactory是不支持一个过滤器配置多个请求头的方式
- 类似的工厂类还有AddRequestParameter和AddResponseHeader,RemoveRequestHeader,RemoveResponseHeader,这几个就不做单独讲解了,使用方式是一样的。一个是添加(清除)请求参数,一个是添加(清除)响应的请求头。
2.3.2、RewritePathGatewayFilterFactory
在Nginx服务启中有一个非常强大的功能就是重写路径,Spring Cloud Gateway默认也提供了这样的功能,这个功能是Zuul没有的。在配置文件中加上以下的配置:
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://example.org
predicates:
- Path=/red/**
filters:
- RewritePath=/red(?<segment>/?.*), $\{segment}
- 对于请求路径 /red/blue,当前的配置在请求到到达前会被重写为 /blue,由于YAML的语法问题,$符号后面应该加上\
- 然后在解释正则表达式前,我们需要学习一下java正则表达式分组的两个概念:
- 命名分组:(?<name>capturing text)
将匹配的子字符串捕获到一个组名称或编号名称中,在获得匹配结果时,可通过分组名进行获取。例如这里的示例,就是将 “capturing text” 捕获到名称为 “name” 的组中
- 引用捕获文本:${name}
将名称为name的命名分组所匹配到的文本内容替换到此处
那么就很好解释官网的这个例子了,对于配置文件中的: - RewritePath=/red(?/?.*), ${segment}详解:
(?<segment>/?.*):
1.?<segment>
名称为 segment 的组
2./?
/出现0次或1次
3…*
任意字符出现0次或多次
合起来就是:将 /?.*匹配到的结果捕获到名称为segment的组中
${segment}:
将名称为 segment 的分组捕获到的文本置换到此处。
注意,\的出现是由于避免 yaml 语法认为这是一个变量(因为在 yaml 中变量的表示法为 ${variable},而这里我们想表达的是字面含义),在 gateway 进行解析时,会被替换为 ${segment}
最后
业务举例:
将 https://spring.io/projects/** 这个路径重写为 https://spring.io/regexp/**
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://spring.io
predicates:
- Path=/projects/**
filters:
- RewritePath=/projects(?<segment>/?.*), /regexp$\{segment}
2.3.3、StripPrefixGatewayFilterFactory
忽略当前路由的第n段路由,结合part使用
如果配置 StripPrefix=1,则去掉第1层路径
例子: /first/order/info 则变为 /order/info
spring:
application:
name: geteway
cloud:
gateway:
routes:
# lb 前缀匹配 /secnod/102/echo /myprefix/102/echo
- id: 102_lb_forward
uri: lb://order-service # 目标服务地址
predicates:
- Path=/secnod/102/*
filters:
- StripPrefix=1 # 转发之前去掉1层路径 取消 /second
- PrefixPath=/myprefix # 路径前面会加myprefix 加上/myprefix
可以去看下StripPrefixGatewayFilterFactory源码:
与之相似的是PrefixPathGatewayFilterFactory,作用是在当前路由的最前端加上一个自定义路由,例如:
cloud:
gateway:
routes:
- id: devilvan-route-prefix
uri: lb://devilvan-caches/caches
predicates:
- Path=/**
filters:
# PrefixPath:给匹配的Path的前面补充一个前缀,若输入路由:playCaffeineCaches
# 则真正的匹配路由为:/cachesController/playCaffeineCaches
- PrefixPath=/cachesController
那么请求路径就会变为:
http://localhost:9527/playCaffeineCaches == localhost:8077/cachesController/playCaffeineCaches
PrefixPathGatewayFilterFactory源码如下:
2.4、自定义过滤器工厂
- 在上面的自定义过滤器中,有没有办法自定义过滤器工厂类呢?这样就可以在配置文件中配置过滤器了。现在需要实现一个过滤器工厂,在打印时间的时候,可以设置参数来决定是否打印请参数。查看GatewayFilterFactory的源码,可以发现GatewayFilterfactory的层级如下:
- 过滤器工厂的顶级接口是GatewayFilterFactory,我们可以直接继承它的两个抽象类来简化开发AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这两个抽象类的区别就是前者接收一个参数(像StripPrefix和我们创建的这种),后者接收两个参数(像AddResponseHeader)。
写法:
例如之前提到的KeyPairGatewayFilterFactory,记住自定义过滤器工厂类名要求为XXXKeyPairGatewayFilterFactory。
①继承AbstractGatewayFilterFactory(如果需要两个参数就继承AbstractNameValueGatewayFilterFactory),重写apply方法
②需要在启动类或者配置类中将该FilterFactory注册进springIOC
③在配置文件中配置,配置时只需要填写xxxGatewayFilterFactory前面的xxx;
cloud:
gateway:
routes:
- id: devilvan-route-prefix
uri: lb://devilvan-caches/caches
predicates:
- Path=/**
filters:
# PrefixPath:给匹配的Path的前面补充一个前缀,若输入路由:playCaffeineCaches
# 则真正的匹配路由为:/cachesController/playCaffeineCaches
- KeyPair
2.5、全局过滤器
- GlobalFilter : 全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。
- gateway内置的Global Filter
Spring Cloud Gateway框架内置的GlobalFilter如下: - 自定义全局过滤器
一般情况下GlobalFilter全局过滤器是程序员使用较多的过滤器;可以用来自定义一些黑名单校验、Token校验等。
步骤:
1,编写一个类 ,实现 两个接口: GlobalFilter, Ordered
GlobalFilter:全局过滤拦截器
Ordered:拦截器的顺序,数字越低,优先级越高
2.向Spring Ioc容器注册GlobalFilter类的Bean;
例如:在过滤器中检查请求中是否携带token请求头。如果token请求头存在则放行;如果token为空或者不存在则设置返回的状态码为:未授权也不再执行下去.
@Component
public class MyGlobalFilter implements GlobalFilter,Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
boolean token = exchange.getRequest().getHeaders().containsKey("token");
System.out.println("----全局过滤器token----"+token);
if (!token){
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
ServerHttpResponse response = exchange.getResponse();
return response.setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}