Spring Cloud 组件之 Zuul

Zuul 是 Netflix 开源的微服务网关,可与 Eureka、Ribbon、Hystrix 等组件配合使用。Spring Cloud 对 Zuul 进行了整合与增强,其默认使用的 HTTP 客户端是 Apache HttpClient,同时也支持 RestClient 或 okhttp3.OkHttpClient。

Zuul 的主要功能是路由转发过滤器。路由功能是微服务架构的关键部分,例如将 /demo/test 请求转发到 demo 服务。Zuul 默认与 Ribbon 结合,实现了客户端负载均衡功能。

本文主要介绍 Zuul 的工作原理、服务搭建流程以及相关核心知识点。

一、工作原理

Zuul 的核心是一系列的 Filters(过滤器),其作用类比 Servlet 框架的 Filter 或 AOP(面向切面编程)。在 Zuul 将请求路由到用户处理逻辑的过程中,这些 Filter 参与各种过滤处理,例如身份验证(Authentication)、负载削减(Load Shedding)等。

image.png

Zuul 使用一系列不同类型的过滤器,使我们能够快速灵活地将功能应用于边缘服务。这些过滤器主要帮助执行以下功能:

  • 身份验证和安全性:确定每个资源的身份验证要求,并拒绝不满足这些要求的请求。
  • 洞察和监控:在边缘跟踪有意义的数据和统计数据,以便提供准确的生产视图。
  • 动态路由:根据需要动态地将请求路由到不同的后端群集。
  • 压力测试:逐渐增加群集的流量以衡量性能。
  • 负载削减(Load Shedding):为每种类型的请求分配容量,并删除超过限制的请求。
  • 静态响应处理:直接在边缘构建一些响应,而不是将它们转发到内部集群。

过滤器的生命周期

image.png

二、Zuul 核心组件

  • zuul-core:Zuul 核心库,包含编译和执行过滤器的核心功能。
  • zuul-simple-webapp:Zuul Web 应用程序示例,展示了如何使用 zuul-core 构建应用程序。
  • zuul-netflix:Lib 包,将其他 Netflix OSS 组件添加到 Zuul 中,例如使用 Ribbon 进行路由请求处理。
  • zuul-netflix-webapp:Webapp 包,它将 zuul-core 和 zuul-netflix 封装成一个简易的 Web 应用工程包。

三、搭建注册到 Eureka 的服务提供者

1. 导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2. 启动类

/**
 * @author Gjing
 */
@SpringBootApplication
@EnableEurekaClient
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

3. 配置文件

server:
  port: 8090
spring:
  application:
    name: demo
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

4. 提供接口供外部访问

/**
 * @author Gjing
 **/
@RestController
public class TestController {

    @GetMapping("/test")
    public ResponseEntity<String> test() {
        return ResponseEntity.ok("ok");
    }
}

四、搭建 Zuul 网关服务

1. 导入 Zuul 和 Eureka 依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

2. 启动类标注注解

/**
 * @author Gjing
 */
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }
}

3. 配置文件

a. 使用 Eureka 负载均衡路由方式

server:
  port: 8080
spring:
  application:
    name: zuul
# 配置 Eureka 地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
# 构建路由地址
zuul:
  routes:
    # 这里可以自定义
    demo2:
      # 匹配的路由规则
      path: /demo/**
      # 路由的目标服务名
      serviceId: demo

b. 不使用 Eureka 负载均衡,采取请求地址路由

server:
  port: 8080
spring:
  application:
    name: zuul
# 配置 Eureka 地址
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
# 构建路由地址
zuul:
  routes:
    # 这里可以自定义
    demo2:
      # 匹配的路由规则
      path: /demo/**
      # 路由的目标服务名
      url: demo
# 关闭使用 Eureka 负载路由
ribbon:
  eureka:
    enabled: false
# 如果不使用 Eureka,需要自己定义路由的那个服务的其他负载服务地址
demo:
  ribbon:
    # 这里写你要路由的 demo 服务的所有负载服务请求地址,本项目只启动一个,因此只写一个
    listOfServers: http://localhost:8090/

c. 完全不依赖 Eureka 使用 Zuul

server:
  port: 8080
spring:
  application:
    name: zuul
# 构建路由地址
zuul:
  routes:
    # 这里可以自定义
    demo2:
      # 匹配的路由规则
      path: /demo/**
      # 路由的目标地址
      url: http://localhost:8090/

4. 启动项目并访问

访问地址:http://localhost:8080/demo/test

五、Zuul 过滤器详解

为了让 API 网关组件更方便地使用,Zuul 在 HTTP 请求生命周期的各个阶段默认实现了一批核心过滤器。它们会在 API 网关服务启动时被自动加载和启动。我们可以在源码中查看和了解它们,它们定义在 spring-cloud-netflix-core 模块的 org.springframework.cloud.netflix.zuul.filters 包下。

在默认启动的过滤器中包含三种不同生命周期的过滤器,这些过滤器非常重要,可以帮助我们理解 Zuul 对外部请求处理的过程,以及帮助我们在此基础上扩展过滤器去完成自身系统需要的功能。

1. Pre 过滤器

  • ServletDetectionFilter

    执行顺序为 -3,是最先被执行的过滤器。该过滤器总是会被执行,主要用来检测当前请求是通过 Spring 的 DispatcherServlet 处理运行的,还是通过 ZuulServlet 来处理运行的。它的检测结果会以布尔类型保存在当前请求上下文的 isDispatcherServletRequest 参数中。这样后续的过滤器中,我们就可以通过 RequestUtils.isDispatcherServletRequest()RequestUtils.isZuulServletRequest() 方法来判断请求处理的源头,以实现后续不同的处理机制。

    一般情况下,发送到 API 网关的外部请求都会被 Spring 的 DispatcherServlet 处理,除了通过 /zuul/* 路径访问的请求会绕过 DispatcherServlet(比如之前我们说的大文件上传),被 ZuulServlet 处理,主要用来应对大文件上传的情况。另外,对于 ZuulServlet 的访问路径 /zuul/*,我们可以通过 zuul.servletPath 参数进行修改。

  • Servlet30WrapperFilter

    执行顺序为 -2,是第二个执行的过滤器。目前的实现会对所有请求生效,主要为了将原始的 HttpServletRequest 包装成 Servlet30RequestWrapper 对象。
  • FormBodyWrapperFilter

    执行顺序为 -1,是第三个执行的过滤器。该过滤器仅对两类请求生效:第一类是 Content-Typeapplication/x-www-form-urlencoded 的请求;第二类是 Content-Typemultipart/form-data 并且是由 Spring 的 DispatcherServlet 处理的请求(用到了 ServletDetectionFilter 的处理结果)。而该过滤器的主要目的是将符合要求的请求体包装成 FormBodyRequestWrapper 对象。
  • DebugFilter

    执行顺序为 1,是第四个执行的过滤器。该过滤器会根据配置参数 zuul.debug.request 和请求中的 debug 参数来决定是否执行过滤器中的操作。而它的具体操作内容是将当前请求上下文中的 debugRoutingdebugRequest 参数设置为 true

    由于在同一个请求的不同生命周期都可以访问到这二个值,所以我们在后续的各个过滤器中可以利用这二个值来定义一些 debug 信息。这样当线上环境出现问题的时候,可以通过参数的方式来激活这些 debug 信息以帮助分析问题。另外,对于请求参数中的 debug 参数,我们可以通过 zuul.debug.parameter 来进行自定义。

  • PreDecorationFilter

    执行顺序是 5,是 Pre 阶段最后被执行的过滤器。该过滤器会判断当前请求上下文中是否存在 forward.doserviceId 参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这二个信息就是根据当前请求的路由信息加载进来的)。

    而当它的具体操作内容就是为当前请求做一些预处理,比如说,进行路由规则的匹配,在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过 RequestContext.getCurrentContext() 来访问这些信息。

    另外,我们还可以在该实现中找到对 HTTP 头请求进行处理的逻辑,其中包含了一些耳熟能详的头域,比如 X-Forwarded-Host, X-Forwarded-Port。另外,对于这些头域是通过 zuul.addProxyHeaders 参数进行控制的,而这个参数默认值是 true,所以 Zuul 在请求跳转时默认会为请求增加 X-Forwarded-* 头域,包括 X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-For, X-Forwarded-Prefix, X-Forwarded-Proto。也可以通过设置 zuul.addProxyHeaders=false 关闭对这些头域的添加动作。

2. Route 过滤器

  • RibbonRoutingFilter

    执行顺序为 10,是 Route 阶段的第一个执行的过滤器。该过滤器只对请求上下文中存在 serviceId 参数的请求进行处理,即只对通过 serviceId 配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用 Ribbon 和 Hystrix 来向服务实例发起请求,并将服务实例的请求结果返回。
  • SimpleHostRoutingFilter

    执行顺序为 100,是 Route 阶段的第二个执行的过滤器。该过滤器只对请求上下文存在 routeHost 参数的请求进行处理,即只对通过 url 配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向 routeHost 参数的物理地址发起请求。从源码中我们可以知道该请求是直接通过 httpclient 包实现的,而没有使用 Hystrix 命令进行包装,所以这类请求并没有线程隔离和断路器的保护。
  • SendForwardFilter

    执行顺序是 500,是 Route 阶段第三个执行的过滤器。该过滤器只对请求上下文中存在的 forward.do 参数进行处理请求,即用来处理路由规则中的 forward 本地跳转装配。

3. Post 过滤器

  • SendErrorFilter

    执行顺序是 0,是 Post 阶段的第一个执行的过滤器。该过滤器仅在请求上下文中包含 error.status_code 参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。而该过滤器的具体逻辑就是利用上下文中的错误信息来组成一个 forward 到 API 网关 /error 错误端点的请求来产生错误响应。
  • SendResponseFilter

    执行顺序为 1000,是 Post 阶段最后执行的过滤器。该过滤器会检查请求上下文中是否包含请求响应相关的头信息、响应数据流或是响应体,只有在包含它们其中一个的时候执行处理逻辑。而该过滤器的处理逻辑就是利用上下文的响应信息来组织需要发送回客户端的响应内容。

使用案例

如果前端发起请求没有带指定请求头将不被允许请求。如果需要读取 Cookie 等敏感信息,要在配置文件中加入 sensitive-headers:(下面有对该配置的详解)。

/**
 * @author Gjing
 **/
@Component
public class GlobalFilter extends ZuulFilter {
    @Override
    public String filterType() {
        // 设置过滤类型
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        // 设置过滤器优先级
        return -4;
    }

    @Override
    public boolean shouldFilter() {
        // 是否需要过滤
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        String token = request.getHeader("token");
        if (StringUtils.isEmpty(token)) {
            // 返回错误信息
            context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            context.setResponseBody(HttpStatus.UNAUTHORIZED.getReasonPhrase());
            context.setSendZuulResponse(false);
            return null;
        }
        return null;
    }
}

项目启动后如果访问不带 Token 请求头,将被拦截,返回 Unauthorized

六、Zuul 核心配置与知识点

1. 路由配置

Zuul 通过与 Eureka 的整合,实现了对服务实例的自动化维护。所以使用服务路由配置的时候,不需要向传统路由配置方式那样为 serviceId 指定具体服务实例地址,只需要通过 zuul.routes.<route>.pathzuul.routes.<route>.serviceId 参数对的方式进行配置即可。

zuul:
  routes:
    # 这里可以自定义
    demo2:
      # 匹配的路由规则
      path: /demo/**
      # 路由的目标服务名
      serviceId: demo

除了 pathserviceId 键值对的配置方式之外,还有一种简单的配置:zuul.routes.<serviceId>=<path>,其中 <serviceId> 用来指定路由的具体服务名,<path> 用来配置匹配的请求表达式。

zuul:
  routes:
    demo: /demo/**

2. 路径匹配

在 Zuul 中,路由匹配的路径表达式采用 Ant 风格定义:

通配符说明
?匹配任意单个字符
*匹配任意数量的字符
**匹配任意数量的字符,支持多级目录

3. 忽略表达式

通过 path 参数定义的 Ant 表达式已经能够完成 API 网关上的路由规则配置功能,但是为了更细粒度和更为灵活地配置路由规则,Zuul 还提供了一个忽略表达式参数 zuul.ignored-patterns。该参数可以用来设置不希望被 API 网关进行路由的 URL 表达式。

zuul:
  routes:
    demo:
      path: /demo/**
      serviceId: demo
  # 不路由 demo2 开头的任意请求
  ignored-patterns: /demo2/**

4. 路由前缀

为了方便地为路由规则增加前缀信息,Zuul 提供了 zuul.prefix 参数来进行设置。比如,希望为网关上的路由规则增加 /api 前缀,那么我们可以在配置文件中增加配置:zuul.prefix=/api

另外,对于代理前缀会默认从路径中移除,我们可以通过设置 zuul.strip-prefix=false(默认为 true,默认为 true 时前缀生效,比如 http://localhost:8080/api/demo/test)来关闭该移除代理前缀的动作。

5. 本地跳转

在 Zuul 实现的 API 网关路由功能中,还支持 forward 形式的服务端跳转配置。实现方式非常简单,只需要通过使用 pathurl 的配置方式就能完成,通过 url 中使用 forward 来指定需要跳转的服务器资源路径。

a. 在 Zuul 服务中添加一个接口

/**
 * @author Gjing
 **/
@RestController
public class HelloController {

    @GetMapping("/test/hello")
    public String test() {
        return "hello zuul";
    }
}

b. 配置文件

zuul:
  routes:
    zuul-service:
      path: /api/**
      serviceId: forward:/test/

启动后访问 http://localhost:8080/api/hello 即可。

6. Cookie 与头信息

默认情况下,Spring Cloud Zuul 在请求路由时,会过滤掉 HTTP 请求头信息中一些敏感信息,防止它们被传递到下游的外部服务器。默认的敏感头信息通过 zuul.sensitiveHeaders 参数定义,默认包括 Cookie, Set-Cookie, Authorization 三个属性。

所以,我们在开发 Web 项目时常用的 Cookie 在 Spring Cloud Zuul 网关中默认是不传递的,这就会引发一个常见的问题:如果我们要将使用了 Spring Security、Shiro 等安全框架构建的 Web 应用通过 Spring Cloud Zuul 构建的网关来进行路由时,由于 Cookie 信息无法传递,我们的 Web 应用将无法实现登录和鉴权。为了解决这个问题,以下介绍两种配置方式:

  • 通过设置全局参数为空来覆盖默认值
zuul:
  routes:
    demo:
      path: /demo/**
      serviceId: demo
  # 允许敏感头,设置为空就行了
  sensitive-headers:
  • 通过指定路由的参数来设置
zuul:
  routes:
    demo:
      path: /demo/**
      serviceId: demo
      # 将指定路由的敏感头设置为空
      sensitiveHeaders:
说明:Zuul 1.x 属于 Netflix OSS 套件,目前已进入维护模式。Spring Cloud 官方推荐使用 Spring Cloud Gateway 作为新一代网关解决方案(基于 Spring WebFlux)。若为新项目选型,建议优先考虑 Spring Cloud Gateway;若维护旧项目,本文内容仍适用于基于 Spring Cloud Finchley 或更早版本的 Zuul 1.x 环境。