springcloud微服务实战_06_API网关服务

倖福魔咒の 提交于 2020-02-29 15:09:04

6.1 zuul 简介

spring cloud zuul

API 网关是一个智能的应用程序,它的定义类似于面向对象设计模式中的 faced 门面模式,它的存在就像是整个微服务架构应用的门面一样,所有的外部客户端访问都需要经过它来进行调度与过滤.它除了要实现请求路由,负载均衡,校验过滤等功能之外,还需要更多的能力,比如与服务治理框架的结合,请求转发时的熔断机制,服务的聚合等一些列高级功能.

首先对于路由规则与服务实例的维护问题. spring cloud zuul 通过与 spring cloud eureka 进行整合,将自身注册为 eureka 服务治理下的应用,同时从 eureka 中获取所有的服务实例. 这样的设计非常巧妙的将服务治理体系中维护的实例信息利用起来,使得维护服务实例的工作交给了服务治理框架自动完成,不需要人工介入.

而对于路由规则的维护,zuul默认会将通过以服务名作为 ContextPath 的方式来路由映射,大部分情况下,这样的默认设置已经可以实现我们大部分的路由需求,除了一些特殊情况还需要做一些特别的配置.

其次对于类似签名校验,登录校验,在微服务架构中的冗余问题.理论上说,这些校验逻辑在本质上与微服务应用自身的业务并没有太大的关系,所以它们完全可以独立成一个单独的服务存在,只是它们被剥离与独立出来之后,并不是给各个微服务使用,而是在 API 网关服务上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验. zuul 提供了一套过滤机制.开发者可以通过使用 zuul 来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然会返回错误的信息. 通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了.

快速入门

构建网关

  • 创建一个基础的 springboot 工程,命名为 api-gateway, build.gradle 引入 spring cloud zuul 依赖
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

  • 创建应用主类,使用 @EnableZuulProxy 注解开启 zuul 的 API 网关服务功能.
// 使用 zuul 反向代理服务器
@EnableZuulProxy
@SpringCloudApplication
public class ApiGatewayApplication {

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

}
  • 在 application.gradle 中配置 zuul 应用的基础信息,如应用名,服务端口等;
spring.application.name=api-gateway
server.port=5555
# 只需要注册一个服务中心,即可同步到其他服务中心去
eureka.client.service-url.defaultZone=http://localhost:8001/eureka/
# 配置服务路由
# 规则: 一般使用服务名称作为服务请求路径的前缀
# 服务1
zuul.routes.hello-service.path=/hello-service/**
zuul.routes.hello-service.service-id=hello-service
# 服务2
zuul.routes.feign-consumer.path=/feign-consumer/**
zuul.routes.feign-consumer.service-id=feign-consumer
# 忽略的表达式
#zuul.ignored-patterns=
#zuul.ignored-services=
#zuul.ignored-headers=
# 路由前缀
zuul.prefix=/api
# 不移除代理前缀, 默认是 TRUE,表示zuul是否移除代理前缀,将前缀同时附加到具体服务的URL上
# 比如我们访问 /api/hello-service/hello 如果不移除前缀, URL最终会映射成为 /api/hello-service/hello, 移除变为 /hello-service/hello
zuul.strip-prefix=true
# zuul 默认会对请求头中的敏感信息头进行过滤,防止其传递到外部服务器.敏感信息头有 cookie,set-cookie,Authorization 三个属性
# 方法一: 对指定路由开启自定义敏感头信息
zuul.routes.hello-service.custom-sensitive-headers=true
# 方法二: 将指定路由的敏感信息头设置为空
zuul.routes.hello-service.sensitive-headers=
# 设置 host 信息,使得网关在路由转发前为请求头设置 host 头信息
zuul.add-host-header=true
# 设置连接时间
zuul.host.connect-timeout-millis=5000
zuul.host.socket-timeout-millis=10000
# 熔断器超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=20000
#hystrix.command.
# 负载均衡器超时时间
ribbon.ConnectTimeout=4000
ribbon.ReadTimeout=4000

完成上述的工作,通过 zuul 实现的 API 网关服务就构建完毕了

6.2 请求路由

传统路由方式

不引用 eureka 的 zuul 通过自定义一些路由规则比如:

zuul.routes.api-a-url.path=/api-a-url/**
zuul.routes.api-a-url.url=http://localhost:8080/

该配置定义了发往 API 网关服务的请求中,所有符合/api-a-url/**规则的访问都将被路由转发到 http://localhost:8080/ 地址上,也就是说,当我们访问 http://localhost:5555/api-a-url/hello 的时候,API 网关服务会将该请求路由到 http://localhost:8080/hello 提供的微服务接口上.

服务路由方式

当引入 eureka 后,我们可以让路由的 path 不是映射到具体的 URL,而是映射到具体的服务. 而具体的 URL 则交给 eureka 的服务发现机制去自动维护,我们称这种方式为面向服务的路由.

  • 引入 eureka 依赖.
dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
}
  • 在 api-gateway 的 application.properties 的配置文件中指定 eureka 的注册中心,并且配置服务路由.
# 只需要注册一个服务中心,即可同步到其他服务中心去
eureka.client.service-url.defaultZone=http://localhost:8001/eureka/
# 配置服务路由
# 规则: 一般使用服务名称作为服务请求路径的前缀
# 服务1
zuul.routes.api-a.path=/api-a/**
zuul.routes.api-a.service-id=hello-service
# 服务2
zuul.routes.api-b.path=/feign-consumer/**
zuul.routes.api-b.service-id=feign-consumer

我们准备了两个微服务应用 hello-service 和 feign-consumer 在上面的配置中分别定义名为api-a 和 api-b 来路由它们. 另外通过 eureka server 服务注册中心的位置, 除了将自己注册成服务之外,同时也让 zuul 能够获取 hello-service 和 feign-consumer 服务的实例清单,以实现 path 映射服务,再从服务中挑选实例来进行请求转发的完整路由机制.

通过上面的映射配置,我们就可以通过以下方式向网关放下下面这些请求:

  • http://locahost:5555/api-a/hello: 该 URL 符合 /api-a/** 规则,由 api-a 路由负责转发,该路由映射的 serviceId 为 hello-service,所以最终 /hello 请求会被转发到 hello-service 服务的某个实例上去了.

  • http://localhost:5555/api-b/feign-consumer: 该 URL 符合 /api-b/** 规则,由 api-b 路由负责转发, 该路由映射的 serviceId 为 feign-consumer,所以最终 /feign-consumer 请求会被转发到 feign-consumer 服务的某个实例上去.

这就是面向服务的路由配置.

6.3 请求过滤

在实现了服务路由功能之后,所有的请求都会毫无保留的转发到了具体的应用并返回结果,为了实现对客户端请求的安全校验和权限控制,最简单粗暴的方法就是为每个微服务应用都实现一套用于签名校验和权限校验的拦截器或过滤器.

不过这种做法并不可取,因为同一个系统中的各种校验逻辑很多情况下是相同的,这样的实现方式会使大量的相似的校验逻辑代码分散到各个微服务中去,冗余代码的出现是我们不希望看到的. 所以比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的权限校验服务. 在完成剥离之后,有不少开发者会直接在微服务应用中通过调用鉴权服务来试下校验,但是这样的做法仅仅只是解决了鉴权逻辑的分离,没有在本质上将这部分不属于冗余的逻辑从原有微服务中拆分出来,冗余的拦截器和过滤器会依然存在.

对于这样的问题,最好的做法是在前置的网关服务去实现这些非业务性质的校验. 我们何不在外部请求到达具体微服务的时候就进行校验和过滤,而不是请求通过网关转发到我们的具体微服务后再去做校验和过滤而导致更长的请求延迟.

我们可以通过继承 zuul 提供的 ZuulFilter 抽象类并且实现四个抽象函数就可以实现对请求的拦截和过滤了.

下面的过滤器代码实现了在请求中获取 accessToken 参数,并且校验.

package com.look.apigateway.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

public class AccessFilter extends ZuulFilter {

  private static final Logger LOGGER = LoggerFactory.getLogger(AccessFilter.class);

  @Override
  public String filterType() {
    return "pre";
  }

  @Override
  public int filterOrder() {
    return 0;
  }

  @Override
  public boolean shouldFilter() {
    return true;
  }

  @Override
  public Object run() throws ZuulException {
    RequestContext ctx = RequestContext.getCurrentContext();
    HttpServletRequest request = ctx.getRequest();
    String accessToken = request.getParameter("accessToken");

    LOGGER.info("send {} request to {}", request.getMethod(), request.getRequestURL().toString());
    // TODO: 2019-04-13 token 处理器
    // redis 或 MySQL ,token 处理器 获取单独的访问一个token鉴权 微服务

    if (StringUtils.isBlank(accessToken)) {
      LOGGER.warn("access token is empty");
      ctx.setSendZuulResponse(false);
      ctx.setResponseStatusCode(401);
      return null;
    }

    LOGGER.info("access token ok");
    return null;
  }
}

ZuulFitler 中四个抽象函数分别定义了如下内容:

  • filterType: 过滤器类型,它决定了过滤器在请求的哪个生命周期中执行.这里定义为 pre,代表会在请求别路由之前执行;
  • filterOrder: 过滤器执行的顺序. 当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行.
  • shouldFilter: 判断该过滤器是否需要被执行.这里我们直接返回 true,因为该过滤器对所有的请求都会生效.实际应用中我们可以根据该函数来指定过滤器的有效范围.
  • run: 过滤器的具体逻辑. 这里我们通过 ctx.setSendZuulResponse(false) 令 zuul 过滤该请求,不对齐进行路由,然后通过 cts.setResponseStatusCode(401) 设置了其返回的错误码,当然也可以进一步优化,比如通过 ctx.setResponseBody(body) 对返回的body内容进行编辑.

在实现了自定义的过滤器后,它不会自动生效,还需要我们创建它的实例. 比如我们在应用主类上加上以下代码.

  /**
   * 自定义的请求过滤器
   *
   * @return
   */
  @Bean
  public AccessFilter accessFilter() {
    return new AccessFilter();
  }

总结:

  • 它作为系统的统一入口,屏蔽了系统内部各个微服务的细节.
  • 它可以与服务治理框架结合,实现自动化的服务实例维护和服务的负载均衡的路由转发.
  • 它可以实现接口权限校验与微服务的业务逻辑的解耦.
  • 通过服务网关中的过滤器,在各个生命周期中取校验请求的内容,将原本在对外服务层做的校验前移,保证微服务的无状态性,降低微服务测试难度,让服务本身更集中关注业务逻辑的处理.

6.4 路由详解

传统路由配置

所谓传统的路由配置就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的影视关系来实现 API 网关对外部请求的路由. 我们需要根据服务实例的数量采用不同的方式配置来实现路由规则.

  • 单实例配置: 通过 zuul.routes.<route>.path 与 zuul.routes.<route>.url 参数对的方式进行配置.
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.url=http://localhost:8080/
  • 多实例的配置: 通过 zuul.routes.<route>.path 与 zuul.routes.<route>.serviceId 参数对的方式进行配置.
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
# 这里由于使用 zuul.routes.<route>.serviceId 指定具体的服务实例名称,
# 默认情况下 ribbon 会根据服务发现机制来获取配置服务名称实例清单,
# 但是这里没有整合 eureka 之类的服务治理框架,所以需要将该参数设置为 false.
ribbon.eureka.enabled=false

# 该参数与 zuul.routes.<route>.serviceId 的参数相对应,开头的 user-service 对应了 serviceId 的值,
# 这两个参数的配置相当于在该应用内部手工维护服务与实例的对应关系.
user-service.ribbion.listOfServers=http://localhost:8080/,http://localhost:8081/

这样的配置实现了对符合 /user-service/** 规则的请求路由转发到 http://localhost:8080/,http://localhost:8081/ 这两个实例地址的路由规则,它与服务路由一样都是采用 zuul.routes.<route>.path 与 zuul.routes.<route>.serviceId 参数,只是这里的 serviceId 是用户手工命令的服务名称,配合 ribbion.listOfServers 参数实现服务与实例的维护.由于存在多个实例,API 网关在路由转发时需要实现负载均衡策略,于是这里还需要 ribbon 的配合. zuul 中自带了对 ribbon 的依赖.我们只需要做一些配置即可.

服务路由配置

通过整合 eureka 的服务治理框架实现对服务实例的自动化维护,我们只需要配置 zuul.routes.<route>.path 与 zuul.routes.<route>.serviceId 参数对即可.

zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service

除了通过 route 与 serviceId 映射的配置方式之外,还有一种更简单的方式: zuul.routes.<serviceId>=<path>, 其中 <serviceId> 用来指定路由的具体服务名,<path>用来配置匹配的请求表达式. 比如下面的例子等价于上面通过 path 与 serviceId 组合使用的配置方式.

zuul.routes.user-service=/user-service/**

服务路由的默认规则

当 zuul 整合 eureka 后,它为 eureka 中的每一个服务都自动创建一个默认路由规则,这些默认规则的 path 会使用 serviceId 配置的服务名称作为请求前缀,就比如上面的例子.

这使得一些我们不希望对外开放的服务也有可能被外部访问到,这时可以使用 zuul.ignored.services 参数来设置一个服务名匹配表达式定义不自动创建路由的规则. 比如设置 zuul.ignored.services=* 时,zuul 将对所有的服务不自动创建路由规则. 这种情况下,我们就要在配置文件中逐个为需要路由的服务添加路由规则(可以使用 path 与 serviceId 的方式,也可以使用更简洁的 zuul.routes.<serviceId>=<path> 方式).

自定义路由规则

有时候我们会通过一组相互配合的微服务定义一个版本标识来方便管理它们的版本关系,根据这个标识我们可以很容易地知道这些服务需要一起配合使用.比如可以采用这样的命名: userservice-v1,userservice-v2. 我们需要为这些不同版本的微服务应用生成以版本号为前缀定义的路由规则. 比如 /v1/userservice/. 这时可以使用 zuul 中自定义服务与路由映射关系的功能. 来实现符合上述规则的微服务自动化地创建类似 /v1/userservice/** 的路由匹配规则.

  /**
   * 自定义的路由规则
   * <p>
   *   例如服务 helloservice-v1
   *   映射的 URL 是  v1/helloservice/**
   * </p>
   * @return
   */
  @Bean
  public PatternServiceRouteMapper patternServiceRouteMapper() {
    // 这里使用了正则表达式分组和反向引用
    // 小括号 () 可以达到对正则表达式进行分组的效果。
    // 模式分组后会在正则表达式中创建反向引用。
    // 反向引用会保存匹配模式分组的字符串片断,这使得我们可以获取并使用这个字符串片断。
    // 在以正则表达式替换字符串的语法中,是通过 $ 来引用分组的反向引用,
    // $0 是匹配完整模式的字符串(注意在 JavaScript 中是用 $& 表示);
    // $1 是第一个分组的反向引用;$2 是第二个分组的反向引用,以此类推。

    // 分组的反向引用副本
    // Java 中可以在小括号中使用 ?<name> 将小括号中匹配的内容保存为一个名字为 name 的副本。
    // 表达式 (?<name>^.+) 意思是模式为将以任何字符开头并且出现一次或多次的字符保存在一个名字叫做 name 的group 中.
    return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+^)", "${version}/${name}");
  }

路径匹配

zuul 中路由匹配的路径表达式采用 Ant 风格定义. 一共有三种通配符:

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

忽略表达式

zuul 提供一个忽略表达式参数 zuul.ignored-patterns.

比如:

zuul.ignored-patterns=/**/hello/**

路由前缀

zuul 提供 zuul.prefix 参数来全局的为路由规则增加前缀,比如我们希望为路由规则增加 /api 前缀,我开门可以配置 zuul.prefix=/api ,另外, 对于代理前缀会默认从路径从移除,我们可以通过设置 zuul.stripPrefix=false 来关闭该移除代理前缀的动作,也可以通过 zuul.routes.<route>.strip-prefix=false 来对指定路由关闭移除代理前缀的动作.

# 路由前缀
zuul.prefix=/api
# 不移除代理前缀, 默认是 TRUE,表示zuul是否移除代理前缀,将前缀同时附加到具体服务的URL上
# 比如我们访问 /api/hello-service/hello 如果不移除前缀, URL最终会映射成为 /api/hello-service/hello, 移除变为 /hello-service/hello
zuul.strip-prefix=true

本地跳转

zuul 还支持 forward 形式的服务端跳转配置,只需要通过使用 path 与 url 的配置方式,通过 url 中使用 forward 来指定需要跳转的服务器资源路径.

zuul.routes.api-b.path=/api-b/**
zuul.routes.api-b.url=forward:/local

上述配置以 /api-b/** 路由规则的请求转发到本地 API 网关服务中以 /local 为前缀的请求上,由 API 网关负责处理.比如 当 API 网关服务收到请求 /api-b/hello,它会被 API 网关转发到网关的 /local/hello 请求上进行本地处理.

Cookie 与请求头

默认情况下,zuul 在请求路由时,会过滤掉 HTPP 请求头信息中的一些敏感信息,防止它们被传递到下游的外部服务器. 默认的敏感信息头通过 zuul.sensitiveHeaders 参数定义,包括 Cookie,Set-Cookie,Authorization 三个属性.所以我们在开发 web 项目时常用的 cookie 在 zuul 网关中默认是不会传递的,这就引发一个问题: 如果我们要将使用 spring security,shiro,等安全框架构建的 web应用通过 zuul 构建的网关来进行路由时,由于 cookie 无法传递,我们的web应用将无法登录与鉴权. 解决这个问题,可以通过以下方式:

  • 通过全局参数来覆盖默认值:

    zuul.sensitiveHeaders=
    

    这种方式不推荐

  • 通过指定路由的参数来配置,有两种方法:

    # 方式一: 对指定路由开启自定义敏感头
    zuul.routes.<router>.customSensitiveHeaders=true
    # 方式二: 将指定路由的敏感头设置为空
    zuul.routes<router>.sensitiveHeaders=
    

    推荐这两种方法,影响比较小.仅针对指定的 web 应用开启敏感信息的传递.

重定向问题

解决了cookie 问题后,通过网关登录我们web 应用,此时又发现一个问题,登录成功之后我们跳转的页面 URL却是具体 web 服务应用实例的地址,而不是通过网关的路由地址.

这是因为由于 spring security 或者 shiro 在登陆完成之后通过重定向方式跳转到登录后的页面.此时登录之后的请求结果状态码为 302,请求响应头中信息中的 Location 指向了具体的服务实例地址,而请求头中的 Host 也指向了具体的服务实例 IP 地址和端口. 所以该问题根本原因在于 zuul 在路由请求时并没有将最初的 host 信息设置正确. 我们需要设置 Host 参数:

# 设置 host 信息,使得网关在路由转发前为请求头设置 host 头信息
zuul.add-host-header=true

Hystrix 与 Ribbon 的支持

zuul 自带线程隔离和断路器的功能,以及微服务调用的负载均衡功能,但是需要注意,当使用 path 与 url 的映射关系来配置路由规则时,对于路由转发的请求不会采用 HystrixCommand 来包装,所以这类路由规则没有线程隔离和熔断器以及负载均衡的能力. 因此我们使用 zuul 时尽量使用服务路由的配置,即 serviceId 与 path 映射.

# 熔断器超时时间 路由转发请求的超时时间,单位毫秒. 当
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=20000
# 负载均衡器超时时间, 路由转发请求时,创建请求连接的超时时间.
# 当 ribbon.ConnectTimeout 小于 hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds 配置值时,若出现路由请求出现连接超时,会自动进行路由重试.
ribbon.ConnectTimeout=4000
# 设置路由转发请求的超时时间.它的超时是对请求建立之后的处理时间.
ribbon.ReadTimeout=4000

如果路由转发请求发生超时(连接超时或者处理超时),只要超时时间小于 hystrix 的命令超时,那么它就会自动发生重试.有些情况下我们不要重试,可以设置以下参数:

# 全部关闭重试
zuul.retryable=false
# 指定路由关闭重试机制
zuul.routes.<route>.retryable=false

6.5 过滤器详解

过滤器

在Spring Cloud Zuul中实现的过滤器必须包含4个基本特征:过滤类型、执行顺序、执行条件、具体操作。这些元素看着似乎非常的熟悉,实际上它就是ZuulFilter接口中定义的四个抽象方法:

String filterType();

int filterOrder();

boolean shouldFilter();

Object run();

它们各自的含义与功能总结如下:

  • filterType:该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了四种不同生命周期的过滤器类型,具体如下:
    • pre:可以在请求被路由之前调用。
    • routing:在路由请求时候被调用。
    • post:在routing和error过滤器之后被调用。
    • error:处理请求时发生错误时被调用。
  • filterOrder:通过int值来定义过滤器的执行顺序,数值越小优先级越高。
  • shouldFilter:返回一个boolean类型来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。
  • run:过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。

请求的生命周期

从上图中,我们可以看到,当外部HTTP请求到达API网关服务的时候,首先它会进入第一个阶段pre,在这里它会被pre类型的过滤器进行处理,该类型的过滤器主要目的是在进行请求路由之前做一些前置加工,比如请求的校验等。在完成了pre类型的过滤器处理之后,请求进入第二个阶段routing,也就是之前说的路由请求转发阶段,请求将会被routing类型过滤器处理,这里的具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例将请求结果都返回之后,routing阶段完成,请求进入第三个阶段post,此时请求将会被post类型的过滤器进行处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在post类型的过滤器中,我们可以对处理结果进行一些加工或转换等内容。另外,还有一个特殊的阶段error,该阶段只有在上述三个阶段中发生异常的时候才会触发,但是它的最后流向还是post类型的过滤器,因为它需要通过post过滤器将最终结果返回给请求客户端(实际实现上还有一些差别,后续介绍)。

核心过滤器

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

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-Type为application/x-www-form-urlencoded的请求,第二类是Content-Type为multipart/form-data并且是由Spring的DispatcherServlet处理的请求(用到了ServletDetectionFilter的处理结果)。而该过滤器的主要目的是将符合要求的请求体包装成FormBodyRequestWrapper对象。
  • DebugFilter:它的执行顺序为1,是第四个执行的过滤器。该过滤器会根据配置参数zuul.debug.request和请求中的debug参数来决定是否执行过滤器中的操作。而它的具体操作内容则是将当前的请求上下文中的debugRouting和debugRequest参数设置为true。由于在同一个请求的不同生命周期中,都可以访问到这两个值,所以我们在后续的各个过滤器中可以利用这两值来定义一些debug信息,这样当线上环境出现问题的时候,可以通过请求参数的方式来激活这些debug信息以帮助分析问题。另外,对于请求参数中的debug参数,我们也可以通过zuul.debug.parameter来进行自定义。
  • PreDecorationFilter:它的执行顺序为5,是pre阶段最后被执行的过滤器。该过滤器会判断当前请求上下文中是否存在forward.to和serviceId参数,如果都不存在,那么它就会执行具体过滤器的操作(如果有一个存在的话,说明当前请求已经被处理过了,因为这两个信息就是根据当前请求的路由信息加载进来的)。而它的具体操作内容就是为当前请求做一些预处理,比如:进行路由规则的匹配、在请求上下文中设置该请求的基本信息以及将路由匹配结果等一些设置信息等,这些信息将是后续过滤器进行处理的重要依据,我们可以通过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关闭对这些头域的添加动作。

route过滤器

  • RibbonRoutingFilter:它的执行顺序为10,是route阶段第一个执行的过滤器。该过滤器只对请求上下文中存在serviceId参数的请求进行处理,即只对通过serviceId配置路由规则的请求生效。而该过滤器的执行逻辑就是面向服务路由的核心,它通过使用Ribbon和Hystrix来向服务实例发起请求,并将服务实例的请求结果返回。
  • SimpleHostRoutingFilter:它的执行顺序为100,是route阶段第二个执行的过滤器。该过滤器只对请求上下文中存在routeHost参数的请求进行处理,即只对通过url配置路由规则的请求生效。而该过滤器的执行逻辑就是直接向routeHost参数的物理地址发起请求,从源码中我们可以知道该请求是直接通过httpclient包实现的,而没有使用Hystrix命令进行包装,所以这类请求并没有线程隔离和断路器的保护。
  • SendForwardFilter:它的执行顺序为500,是route阶段第三个执行的过滤器。该过滤器只对请求上下文中存在forward.to参数的请求进行处理,即用来处理路由规则中的forward本地跳转配置。

post过滤器

  • SendErrorFilter:它的执行顺序为0,是post阶段第一个执行的过滤器。该过滤器仅在请求上下文中包含error.status_code参数(由之前执行的过滤器设置的错误编码)并且还没有被该过滤器处理过的时候执行。而该过滤器的具体逻辑就是利用请求上下文中的错误信息来组织成一个forward到API网关/error错误端点的请求来产生错误响应。
  • SendResponseFilter:它的执行顺序为1000,是post阶段最后执行的过滤器。该过滤器会检查请求上下文中是否包含请求响应相关的头信息、响应数据流或是响应体,只有在包含它们其中一个的时候就会执行处理逻辑。而该过滤器的处理逻辑就是利用请求上下文的响应信息来组织需要发送回客户端的响应内容。

禁用过滤器

默认情况下,无论是核心过滤器还是自定义过滤器,只要是在 API 网关服务中为它们创建了实例i,它们都是启用状态的.而如果我们不想使用有些过滤器,要么我们会重新该过滤器的 shouldFilter() 逻辑,让它返回 false, 要么就需要使用指定参数的形式.

zuul.<SimpleClassName>.<filterType>.disable=true
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!