SpringCloud系列:spring cloud gateway之filter篇
在上一篇文章详细的介绍了Gateway的Predict,Predict决定了请求由哪一个路由处理,在路由处理之前,需要经过“pre”类型的过滤器处理,处理返回响应之后,可以由“post”类型的过滤器处理。
filter的作用和生命周期
由filter工作流程点,可以知道filter有着非常重要的作用,在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。首先需要弄清一点为什么需要网关这一层,这就不得不说下filter的作用了。
作用
当我们有很多个服务时,比如下图中的user-service、goods-service、sales-service等服务,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。
对于这样重复的工作,有没有办法做的更好,答案是肯定的。在微服务的上一层加一个全局的权限控制、限流、日志输出的Api Gatewat服务,然后再将请求转发到具体的业务服务层。这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。
生命周期
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。
gateway filter
过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。过滤器可以限定作用在某些特定请求路径上。Spring Cloud Gateway包含许多内置的GatewayFilter工厂。
GatewayFilter工厂同上一篇介绍的Predicate工厂类似,都是在配置文件application.yml中配置,遵循了约定大于配置的思想,只需要在配置文件配置GatewayFilter Factory的名称,而不需要写全部的类名,比如AddRequestHeaderGatewayFilterFactory只需要在配置文件中写AddRequestHeader,而不是全部类名。在配置文件中配置的GatewayFilter Factory最终都会相应的过滤器工厂类处理。
Spring Cloud Gateway 内置的过滤器工厂一览表如下:
现在挑几个常见的过滤器工厂来讲解,每一个过滤器工厂在官方文档都给出了详细的使用案例,如果不清楚的还可以在org.springframework.cloud.gateway.filter.factory看每一个过滤器工厂的源码。
AddRequestHeader GatewayFilter Factory
创建工程,引入相关的依赖,包括spring boot 版本2.0.5,spring Cloud版本Finchley,gateway依赖如下:
1. <dependency>
2.
<groupId>
org.springframework.cloud
</groupId>
3.
<artifactId>
spring-cloud-starter-gateway
</artifactId>
4. </dependency>
在工程的配置文件中,加入以下的配置:
1. server:
2. port:
8081
3. spring:
4. profiles:
5. active:add_request_header_route
6.
7. ---
8. spring:
9. cloud:
10. gateway:
11. routes:
12. -id:add_request_header_route
13. uri:http:
//httpbin.org:80/get
14. filters:
15. -
AddRequestHeader
=X-
Request
-
Foo
,
Bar
16. predicates:
17. -
After
=
2017
-
01
-
20T17
:
42
:
47.789
-
07
:
00
[
America
/
Denver
]
18. profiles:add_request_header_route
在上述的配置中,工程的启动端口为8081,配置文件为addrequestheaderroute,在addrequestheaderroute配置中,配置了roter的id为addrequestheader_route,路由地址为http://httpbin.org:80/get,该router有AfterPredictFactory,有一个filter为AddRequestHeaderGatewayFilterFactory(约定写成AddRequestHeader),AddRequestHeader过滤器工厂会在请求头加上一对请求头,名称为X-Request-Foo,值为Bar。为了验证AddRequestHeaderGatewayFilterFactory是怎么样工作的,查看它的源码,AddRequestHeaderGatewayFilterFactory的源码如下:
1. public
class
AddRequestHeaderGatewayFilterFactory
extends
AbstractNameValueGatewayFilterFactory
{
2.
3.
@Override
4.
public
GatewayFilter
apply(
NameValueConfig
config){
5.
return
(exchange,chain)->{
6.
ServerHttpRequest
request =exchange.getRequest().mutate()
7. .header(config.getName(),config.getValue())
8. .build();
9.
10.
return
chain.filter(exchange.mutate().request(request).build());
11. };
12. }
13.
14. }
由上面的代码可知,根据旧的ServerHttpRequest创建新的ServerHttpRequest ,在新的ServerHttpRequest加了一个请求头,然后创建新的ServerWebExchange ,提交过滤器链继续过滤。
启动工程,通过curl命令来模拟请求:
1. curl localhost:
8081
最终显示了从 http://httpbin.org:80/get得到了请求,响应如下:
1. {
2.
"args"
:{},
3.
"headers"
:{
4.
"Accept"
:
"*/*"
,
5.
"Connection"
:
"close"
,
6.
"Forwarded"
:
"proto=http;host=\"localhost:8081\";for=\"0:0:0:0:0:0:0:1:56248\""
,
7.
"Host"
:
"httpbin.org"
,
8.
"User-Agent"
:
"curl/7.58.0"
,
9.
"X-Forwarded-Host"
:
"localhost:8081"
,
10.
"X-Request-Foo"
:
"Bar"
11. },
12.
"origin"
:
"0:0:0:0:0:0:0:1, 210.22.21.66"
,
13.
"url"
:
"http://localhost:8081/get"
14. }
可以上面的响应可知,确实在请求头中加入了X-Request-Foo这样的一个请求头,在配置文件中配置的AddRequestHeader过滤器工厂生效。
跟AddRequestHeader过滤器工厂类似的还有AddResponseHeader过滤器工厂,在此就不再重复。
RewritePath GatewayFilter Factory
在Nginx服务启中有一个非常强大的功能就是重写路径,Spring Cloud Gateway默认也提供了这样的功能,这个功能是Zuul没有的。在配置文件中加上以下的配置:
1. spring:
2. profiles:
3. active:rewritepath_route
4. ---
5. spring:
6. cloud:
7. gateway:
8. routes:
9. -id:rewritepath_route
10. uri:https:
//blog.csdn.net
11. predicates:
12. -
Path
=
/foo/
**
13. filters:
14. -
RewritePath
=
/foo/
(?<segment>.*),/$\{segment}
15. profiles:rewritepath_route
上面的配置中,所有的/foo/*开始的路径都会命中配置的router,并执行过滤器的逻辑,在本案例中配置了RewritePath过滤器工厂,此工厂将/foo/(?.)重写为{segment},然后转发到https://blog.csdn.net。比如在网页上请求localhost:8081/foo/forezp,此时会将请求转发到https://blog.csdn.net/forezp的页面,比如在网页上请求localhost:8081/foo/forezp/1,页面显示404,就是因为不存在https://blog.csdn.net/forezp/1这个页面。
自定义过滤器
Spring Cloud Gateway内置了19种强大的过滤器工厂,能够满足很多场景的需求,那么能不能自定义自己的过滤器呢,当然是可以的。在spring Cloud Gateway中,过滤器需要实现GatewayFilter和Ordered2个接口。写一个RequestTimeFilter,代码如下:
1. public
class
RequestTimeFilter
implements
GatewayFilter
,
Ordered
{
2.
3.
private
static
final
Log
log =
LogFactory
.getLog(
GatewayFilter
.
class
);
4.
private
static
final
String
REQUEST_TIME_BEGIN =
"requestTimeBegin"
;
5.
6.
@Override
7.
public
Mono
<
Void
>filter(
ServerWebExchange
exchange,
GatewayFilterChain
chain){
8.
9. exchange.getAttributes().put(REQUEST_TIME_BEGIN,
System
.currentTimeMillis());
10.
return
chain.filter(exchange).then(
11.
Mono
.fromRunnable(()->{
12.
Long
startTime =exchange.getAttribute(REQUEST_TIME_BEGIN);
13.
if
(startTime !=
null
){
14. log.info(exchange.getRequest().getURI().getRawPath()+
": "
+(
System
.currentTimeMillis()-startTime)+
"ms"
);
15. }
16. })
17. );
18.
19. }
20.
21.
@Override
22.
public
int
getOrder(){
23.
return
0
;
24. }
25. }
在上面的代码中,Ordered中的int getOrder()方法是来给过滤器设定优先级别的,值越大则优先级越低。还有有一个filterI(exchange,chain)方法,在该方法中,先记录了请求的开始时间,并保存在ServerWebExchange中,此处是一个“pre”类型的过滤器,然后再chain.filter的内部类中的run()方法中相当于"post"过滤器,在此处打印了请求所消耗的时间。然后将该过滤器注册到router中,代码如下:
1.
@Bean
2.
public
RouteLocator
customerRouteLocator(
RouteLocatorBuilder
builder){
3.
// @formatter:off
4.
return
builder.routes()
5. .route(r ->r.path(
"/customer/**"
)
6. .filters(f ->f.filter(
new
RequestTimeFilter
())
7. .addResponseHeader(
"X-Response-Default-Foo"
,
"Default-Bar"
))
8. .uri(
"http://httpbin.org:80/get"
)
9. .order(
0
)
10. .id(
"customer_filter_router"
)
11. )
12. .build();
13.
// @formatter:on
14. }
重启程序,通过curl命令模拟请求:
1. curl localhost:
8081
/customer/
123
在程序的控制台输出一下的请求信息的日志:
1. 2018
-
11
-
16
15
:
02
:
20.177
INFO
20488
---[ctor-http-nio-
3
]o.s.cloud.gateway.filter.
GatewayFilter
:
/customer/
123
:
152ms
自定义过滤器工厂
在上面的自定义过滤器中,有没有办法自定义过滤器工厂类呢?这样就可以在配置文件中配置过滤器了。现在需要实现一个过滤器工厂,在打印时间的时候,可以设置参数来决定是否打印请参数。查看GatewayFilterFactory的源码,可以发现GatewayFilterfactory的层级如下:
过滤器工厂的顶级接口是GatewayFilterFactory,我们可以直接继承它的两个抽象类来简化开发AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这两个抽象类的区别就是前者接收一个参数(像StripPrefix和我们创建的这种),后者接收两个参数(像AddResponseHeader)。
过滤器工厂的顶级接口是GatewayFilterFactory,有2个两个较接近具体实现的抽象类,分别为AbstractGatewayFilterFactory和AbstractNameValueGatewayFilterFactory,这2个类前者接收一个参数,比如它的实现类RedirectToGatewayFilterFactory;后者接收2个参数,比如它的实现类AddRequestHeaderGatewayFilterFactory类。现在需要将请求的日志打印出来,需要使用一个参数,这时可以参照RedirectToGatewayFilterFactory的写法。
1. public
class
RequestTimeGatewayFilterFactory
extends
AbstractGatewayFilterFactory
<
RequestTimeGatewayFilterFactory
.
Config
>{
2.
3.
4.
private
static
final
Log
log =
LogFactory
.getLog(
GatewayFilter
.
class
);
5.
private
static
final
String
REQUEST_TIME_BEGIN =
"requestTimeBegin"
;
6.
private
static
final
String
KEY =
"withParams"
;
7.
8.
@Override
9.
public
List
<
String
>shortcutFieldOrder(){
10.
return
Arrays
.asList(KEY);
11. }
12.
13.
public
RequestTimeGatewayFilterFactory
(){
14.
super
(
Config
.
class
);
15. }
16.
17.
@Override
18.
public
GatewayFilter
apply(
Config
config){
19.
return
(exchange,chain)->{
20. exchange.getAttributes().put(REQUEST_TIME_BEGIN,
System
.currentTimeMillis());
21.
return
chain.filter(exchange).then(
22.
Mono
.fromRunnable(()->{
23.
Long
startTime =exchange.getAttribute(REQUEST_TIME_BEGIN);
24.
if
(startTime !=
null
){
25.
StringBuilder
sb =
new
StringBuilder
(exchange.getRequest().getURI().getRawPath())
26. .append(
": "
)
27. .append(
System
.currentTimeMillis()-startTime)
28. .append(
"ms"
);
29.
if
(config.isWithParams()){
30. sb.append(
" params:"
).append(exchange.getRequest().getQueryParams());
31. }
32. log.info(sb.toString());
33. }
34. })
35. );
36. };
37. }
38.
39.
40.
public
static
class
Config
{
41.
42.
private
boolean
withParams;
43.
44.
public
boolean
isWithParams(){
45.
return
withParams;
46. }
47.
48.
public
void
setWithParams(
boolean
withParams){
49.
this
.withParams=withParams;
50. }
51.
52. }
53. }
在上面的代码中apply(Config config)方法内创建了一个GatewayFilter的匿名类,具体的实现逻辑跟之前一样,只不过加了是否打印请求参数的逻辑,而这个逻辑的开关是config.isWithParams()。静态内部类类Config就是为了接收那个boolean类型的参数服务的,里边的变量名可以随意写,但是要重写ListshortcutFieldOrder()这个方法。。
需要注意的是,在类的构造器中一定要调用下父类的构造器把Config类型传过去,否则会报ClassCastException
最后,需要在工程的启动文件Application类中,向Srping Ioc容器注册RequestTimeGatewayFilterFactory类的Bean。
1.
@Bean
2.
public
RequestTimeGatewayFilterFactory
elapsedGatewayFilterFactory(){
3.
return
new
RequestTimeGatewayFilterFactory
();
4. }
然后可以在配置文件中配置如下:
1. spring:
2. profiles:
3. active:elapse_route
4.
5. ---
6. spring:
7. cloud:
8. gateway:
9. routes:
10. -id:elapse_route
11. uri:http:
//httpbin.org:80/get
12. filters:
13. -
RequestTime
=
false
14. predicates:
15. -
After
=
2017
-
01
-
20T17
:
42
:
47.789
-
07
:
00
[
America
/
Denver
]
16. profiles:elapse_route
启动工程,在浏览器***问localhost:8081?name=forezp,可以在控制台上看到,日志输出了请求消耗的时间和请求参数。
global filter
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”则不转发路由,否则执行正常的逻辑。代码如下:
1. public
class
TokenFilter
implements
GlobalFilter
,
Ordered
{
2.
3.
Logger
logger=
LoggerFactory
.getLogger(
TokenFilter
.
class
);
4.
@Override
5.
public
Mono
<
Void
>filter(
ServerWebExchange
exchange,
GatewayFilterChain
chain){
6.
String
token =exchange.getRequest().getQueryParams().getFirst(
"token"
);
7.
if
(token ==
null
||token.isEmpty()){
8. logger.info(
"token is empty..."
);
9. exchange.getResponse().setStatusCode(
HttpStatus
.UNAUTHORIZED);
10.
return
exchange.getResponse().setComplete();
11. }
12.
return
chain.filter(exchange);
13. }
14.
15.
@Override
16.
public
int
getOrder(){
17.
return
-
100
;
18. }
19. }
在上面的TokenFilter需要实现GlobalFilter和Ordered接口,这和实现GatewayFilter很类似。然后根据ServerWebExchange获取ServerHttpRequest,然后根据ServerHttpRequest中是否含有参数token,如果没有则完成请求,终止转发,否则执行正常的逻辑。
然后需要将TokenFilter在工程的启动类中注入到Spring Ioc容器中,代码如下:
1. @Bean
2. public
TokenFilter
tokenFilter(){
3.
return
new
TokenFilter
();
4. }
启动工程,使用curl命令请求:
1. curl localhost:
8081
/customer/
123
可以看到请没有被转发,请求被终止,并在控制台打印了如下日志:
1. 2018
-
11
-
16
15
:
30
:
13.543
INFO
19372
---[ctor-http-nio-
2
]gateway.
TokenFilter
:token
is
empty...
上面的日志显示了请求进入了没有传“token”的逻辑。
总结
本篇文章讲述了Spring Cloud Gateway中的过滤器,包括GatewayFilter和GlobalFilter。从官方文档的内置过滤器讲起,然后讲解自定义GatewayFilter、GatewayFilterFactory以及自定义的GlobalFilter。有很多内置的过滤器并没有讲述到,比如限流过滤器,这个我觉得是比较重要和大家关注的过滤器,将在之后的文章讲述。
参考资料
https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.1.0.M1/single/spring-cloud-gateway.html
https://www.jianshu.com/p/eb3a67291050
https://blog.csdn.net/qq_36236890/article/details/80822051
https://windmt.com/2018/05/08/spring-cloud-14-spring-cloud-gateway-filter
源码下载
https://github.com/forezp/SpringCloudLearning/tree/master/sc-f-gateway-predicate
原创作者:方志朋
方志朋简介:SpringCloud中国社区联合创始人,博客访问量突破一千万,爱好开源,热爱分享,活跃于各大社区,保持着非常强的学习驱动力,终身学习践行者,终身学习受益者。目前就职于国内某家知名互联网保险公司,担任DEVOPS工程师,对微服务领域和持续集成领域研究较深,精通微服务框架SpringCloud
- 点赞
- 收藏
- 关注作者
评论(0)