在上篇文章中我们了解了 Spring Cloud Zuul 作为网关所具备的最基本功能:路由(Router),下面我们将关注 Spring Cloud Zuul 的另一核心功能:过滤器(Filter)。
Filter 的作用
我们已经能够实现请求的路由功能,所以我们的微服务应用提供的接口就可以通过统一的 API 网关入口被客户端访问到了。
但是,每个客户端用户请求微服务应用提供的接口时,它们的访问权限往往都需要有一定的限制,系统并不会将所有的微服务接口都对它们开放。然而,目前的服务路由并没有限制权限这样的功能,所有请求都会被毫无保留地转发到具体的应用并返回结果。
为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后的系统维护难度,因为同一个系统中的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式会使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。所以,比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。在完成了剥离之后,有不少开发者会直接在微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了鉴权逻辑的分离,并没有在本质上将这部分不属于业余的逻辑拆分出原有的微服务应用,冗余的拦截器或过滤器依然会存在。
对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的加入,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用的接口开发和测试复杂度也得到了相应的降低。
Filter 的生命周期
Filter 的生命周期有 4 个,分别是 “PRE”、“ROUTING”、“POST” 和“ERROR”,整个生命周期可以用下图来表示
Zuul 大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
- PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR:在其他阶段发生错误时执行该过滤器。 除了默认的过滤器类型,Zuul 还允许我们创建自定义的过滤器类型。例如,我们可以定制一种 STATIC 类型的过滤器,直接在 Zuul 中生成响应,而不将请求转发到后端的微服务。
Zuul 中默认实现的 Filter
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理 Servlet 的类型 |
pre | -2 | Servlet30WrapperFilter | 包装 HttpServletRequest 请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId 请求转发 |
route | 100 | SimpleHostRoutingFilter | url 请求转发 |
route | 500 | SendForwardFilter | forward 请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
如何禁用指定的 Filter
可以在 application.yml 中配置需要禁用的 filter,格式为zuul.<SimpleClassName>.<filterType>.disable=true
。
比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter
就设置
zuul:
SendResponseFilter:
post:
disable: true
自定义 Filter
首先自定义一个 Filter,继承 ZuulFilter 抽象类,在 run() 方法中添加具体业务逻辑,具体如下:
1 package com.carry.springcloud;
2
3 import javax.servlet.http.HttpServletRequest;
4
5 import com.netflix.zuul.ZuulFilter;
6 import com.netflix.zuul.context.RequestContext;
7
8 public class MyFilter extends ZuulFilter {
9
10 /**
11 * 过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。 这里定义为pre,代表会在请求被路由之前执行。
12 *
13 * @return
14 */
15 @Override
16 public String filterType() {
17 return "pre";
18 }
19
20 /**
21 * filter执行顺序,通过数字指定。 数字越大,优先级越低。
22 *
23 * @return
24 */
25 @Override
26 public int filterOrder() {
27 return 0;
28 }
29
30 /**
31 * 判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效。 实际运用中我们可以利用该函数来指定过滤器的有效范围。
32 *
33 * @return
34 */
35 @Override
36 public boolean shouldFilter() {
37 return true;
38 }
39
40 /**
41 * 过滤器的具体逻辑
42 *
43 * @return
44 */
45 @Override
46 public Object run() {
47 RequestContext ctx = RequestContext.getCurrentContext();
48 HttpServletRequest request = ctx.getRequest();
49
50 String token = request.getParameter("token");
51 if (token == null || token.isEmpty()) {
52 ctx.setSendZuulResponse(false);
53 ctx.setResponseStatusCode(401);
54 ctx.setResponseBody("token is empty");
55 }
56 return null;
57 }
58 }
在上面实现的过滤器代码中,我们通过继承ZuulFilter
抽象类并重写了下面的四个方法来实现自定义的过滤器。这四个方法分别定义了:
filterType()
:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为pre
,代表会在请求被路由之前执行。filterOrder()
:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。通过数字指定,数字越大,优先级越低。shouldFilter()
:判断该过滤器是否需要被执行。这里我们直接返回了true
,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。run()
:过滤器的具体逻辑。这里我们通过ctx.setSendZuulResponse(false)
令 Zuul 过滤该请求,不对其进行路由,然后通过ctx.setResponseStatusCode(401)
设置了其返回的错误码,当然我们也可以进一步优化我们的返回,比如,通过ctx.setResponseBody(body)
对返回 body 内容进行编辑等。
在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的 Bean 才能启动该过滤器,比如,在应用主类中增加如下内容:
1 package com.carry.springcloud;
2
3 import org.springframework.boot.SpringApplication;
4 import org.springframework.boot.autoconfigure.SpringBootApplication;
5 import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
6 import org.springframework.context.annotation.Bean;
7
8 @EnableZuulProxy
9 @SpringBootApplication
10 public class ServiceApiGatewayApplication {
11
12 public static void main(String[] args) {
13 SpringApplication.run(ServiceApiGatewayApplication.class, args);
14 }
15
16 @Bean
17 public MyFilter myFilter() {
18 return new MyFilter();
19 }
20 }
重新启动service-api-gateway,并发起下面的请求,对上面定义的过滤器做一个验证:
- 访问 http://localhost:9100/service-consumer-ribbon/getPoducerInfo 返回
token is empty
- 访问 http://localhost:9100/service-consumer-ribbon/getPoducerInfo?token=123 正确路由到service-consumer-ribbon的
/getPoducerInfo
接口