Spring Cloud Zuul 过滤器

Spring Cloud Zuul 除了可以实现请求的路由功能,还有一个重要的功能就是过滤器。Zuul 的路由功能让所有的微服务提供的接口有统一的网关入口,但并不是所有的接口都是对外完全开发的,它们的访问权限一般都有一定的限制。那我们可以在每个服务都加上对应的校验和权限鉴定,那这些通常都是用过滤器或拦截器实现的,而且一个系统的各个服务的校验也大都是相似,这些相似的校验逻辑打码在每个服务都会有一份,不仅冗余且维护麻烦。更好的办法就是在请求的最前端统一去做这样的事情,而统一的 API 服务网关入口就是合适的选择

Zuul 可以通过定义过滤器来实现请求的拦截和过滤,而它本身的大部分功能也是通过过滤器实现的

过滤器

在 Zuul 中自定义过滤器需要继承抽象类 ZuulFilter,需要实现以下4个方法:

1
2
3
4
5
6
7
String filterType()// 过滤器类型

int filterOrder()// 执行顺序,数值越小优先级越高

boolean shouldFilter()// 执行过滤器的条件

Object run() throws ZuulException; // 具体的过滤操作

从名字我们也能知道各个方法的作用,下面来了解下过滤器类型和过滤器的生命周期,以及自定义过滤器的使用

过滤器类型和生命周期

Zuul 定义了4种不同的过滤器类型,对应着请求的典型生命周期

  • PRE: 在请求被路由之前执行。可以用于请求身份验证、选择源服务器和记录调试信息
  • ROURING: 该过滤器将请求路由到服务。用于构建和发送给微服务的请求(使用 Apache HttpClient 或 Netflix Ribbon 请求服务)
  • POST: 在请求被路由到服务之后执行。可以用于向响应添加标准的 HTTP Header、收集统计数据和指标,以及将响应从源服务发送到客户端
  • ERROR: 该过滤器在其他阶段发生错误时执行

除了默认的过滤器流,Zuul 还允许我们创建自定义过滤器类型并显式地执行它们。例如,我们可以自定义一个 STATIC 类型的过滤器,它在Zuul 中生成响应,而不是将请求转发到后端的服务

下面是 Zuul 的生命周期图,描述着各种类型的过滤器的执行顺序(图片来源于 Zuul Wiki

Zuul 的生命周期图

Spring Cloud Zuul 的过滤器

Spring Cloud Zuul 作为服务网关的大部分功能都是通过过滤器实现的,它在请求的各个阶段实现了一系列的过滤器,在 Spring Cloud Zuul 网关服务启动时自动加载和启用。实现的这些过滤器是在 spring-cloud-netflix-zuul 模块中的 org.springframework.cloud.netflix.zuul.filters 包下面

下面介绍部分过滤器的功能

Pre filters

filter order 说明
ServletDetectionFilter -3 检测请求是否通过 Spring 调度程序,即判断请求是交由 Spring DispatcherServlet 处理,还是 ZuulServlet 处理(主要是用于大文件上传)
Servlet30WrapperFilter -2 把原始 HttpServletRequest 包装成 Servlet30RequestWrapper 对象
FormBodyWrapperFilter -1 解析表单数据并为下游服务重新编码
DebugFilter 1 如果设置 debug 请求参数,则此过滤器将RequestContext.setDebugRouting() 和 RequestContext.setDebugRequest() 设置为true
PreDecorationFilter 5 根据提供的 RouteLocator 确定路由的位置和方式,它还为下游请求设置各种与代理相关的头文件

Route filters

filter order 说明
RibbonRoutingFilter 10 使用 Ribbon、Hystrix 和 可插拔 HTTP客户机发送请求。只对 RequestContext 存在 serviceId 参数的请求进行处理,即只对通过 serviceId配置路由规则的请求路由。可以使用不同的 HTTP 客户端:HttpClient、OkHttpClient、Netflix Ribbon HTTP client
SimpleHostRoutingFilter 100 通过 Apache HttpClient 发送请求到预定的 url,这些 url 可以在 RequestContext.getRouteHost() 中找到,即只对通过 url 配置路由规则的请求路由
SendForwardFilter 500 通过使用 Servlet RequestDispatcher 转发请求。用于转发请求到当前应用的端点

Post filters

filter order 说明
LocationRewriteFilter 900 负责将 Location header 重写为 Zuul URL
SendResponseFilter 1000 将代理请求的响应写入当前响应

Error filters

filter order 说明
SendErrorFilter 0 利用请求上下文中的错误信息来组织成一个 forward 到 /error 错误端点的请求来产生错误响应

禁用过滤器

默认情况下,这些过滤器在代理和服务器模式下都是启用的。如果在某些场景下,禁用某个过滤器,可以设置 zuul.<SimpleClassName>.<filterType>.disable=true。例如,要禁用org.springframework.cloud.netflix.zuul.filter.post.sendresponsefilter,设置 zuul.SendResponseFilter.post.disable=true

自定义过滤器

创建一个 Spring Boot 项目 zuul-filters,Zuul 的服务网关路由配置可以见 Spring Cloud Zuul 构建微服务网关

application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
spring:
application:
name: zuul-filters
server:
port: 8090
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/

zuul:
ignoredServices: '*'
routes:
product:
path: /product/**
serviceId: product-service

management:
endpoints:
web:
exposure:
include: '*'

自定义一个过滤器 AccessFilter,对请求中没有 accessToken 参数的请求,返回 401拒绝访问

AccessFilter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Log4j2
public class AccessFilter extends ZuulFilter {

@Override
public int filterOrder() {
// run before PreDecoration
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1;
}

@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}

@Override
public boolean shouldFilter() {
RequestContext ctx = RequestContext.getCurrentContext();
// a filter has already forwarded
// a filter has already determined serviceId
return !ctx.containsKey(FilterConstants.FORWARD_TO_KEY)
&& !ctx.containsKey(FilterConstants.SERVICE_ID_KEY);
}

@Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();

log.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());

String token = request.getParameter("accessToken");
if(StringUtils.isBlank(token)) {
log.warn("access token is empty");
// 过滤该请求,不对其进行路由
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
return null;
}
log.info("access token ok");
return null;
}
}

实现自定义过滤器后,把它添加到 Spring 的 Beans 中

ZuulFiltersApplication.java
1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
@EnableZuulProxy
public class ZuulFiltersApplication {

public static void main(String[] args) {
SpringApplication.run(ZuulFiltersApplication.class, args);
}

@Bean
public AccessFilter accessFilter() {
return new AccessFilter();
}
}

下面我们来测试下结果,启动项目 eureka-serverproduct-serivce(作为代理的服务)、zuul-filters

访问 http://localhost:8090/product/product/1 返回 401
访问 http://localhost:8090/product/product/1?accessToken=111 会正常路由到 product-service 的 /product/1

参考代码见:demo

过滤器管理端点

@EnableZuulProxy 注解配合 Spring Boot Actuator,Zuul 会暴露额外的两个管理端点:RoutesFilters。分别是关于路由和过滤器的端点(服务路由的端点在这里介绍 Spring Cloud Zuul 构建微服务网关

spring-cloud-starter-netflix-zuul 已经依赖了 spring-boot-starter-actuator,所以上面的工程已经包含了路由管理的功能。关于过滤器的管理端点的路径为 /filters

访问路径 http://localhost:8090/actuator/filters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{
"error": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.post.SendErrorFilter",
"order": 0,
"disabled": false,
"static": true
}
],
"post": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter",
"order": 1000,
"disabled": false,
"static": true
}
],
"pre": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.DebugFilter",
"order": 1,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.FormBodyWrapperFilter",
"order": -1,
"disabled": false,
"static": true
},
{
"class": "com.turbosnail.zuul.filter.AccessFilter",
"order": 4,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.Servlet30WrapperFilter",
"order": -2,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.ServletDetectionFilter",
"order": -3,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.pre.PreDecorationFilter",
"order": 5,
"disabled": false,
"static": true
}
],
"route": [
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.SimpleHostRoutingFilter",
"order": 100,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter",
"order": 10,
"disabled": false,
"static": true
},
{
"class": "org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter",
"order": 500,
"disabled": false,
"static": true
}
]
}

访问404是因为没有暴露端点,可以设置 management.endpoints.web.exposure.include: ‘*’