API 网关服务:Spring Cloud Zuul(二):路由详解、Cookie 与头信息

实践出于《Spring Cloud 微服务实战》 - 瞿永超 著

路由详解

传统路由配置

  传统路由配置就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现 API 网关对外部请求路由。

  • 单实例配置:通过zuul.routes..path 与 zuul.routes..url参数对的方式进行配置,比如之前介绍的:
zuul:
  routes:
    # 传统路由配置
    api-a-url:
      path: /api-a-url/**
      url: http://localhost:8080/
  • 多实例配置: 通过 zuul.routes..path 与 zuul.routes..serviceId 参数对的方式进行配置,比如:
zuul:
  routes:
    # 传统路由配置
    user-service:
      path: /user-service/**
      serviceId: user-service

# 关闭ribbon的注册由本地获取 
ribbon:
  eureka:
    enabled: false

# 应用内部手工维护服务于实例的对应关系
user-service:
  ribbon:
    listOfServices: http://localhost:8080/, http://localhost:8081/   

  不论是单实例还是多实例的配置方式,我们都需要为每一对映射关系指定一个名称,也就是上面的,每一个 route 对应了一条路由规则。每条路由规则都需要通过 path 属性来定义一个用来匹配客户端请求的路径表达式。并通过 url 或 serivceId 属性来指定请求表达式映射具体实例地址或服务名。

服务路由配置

  Spring Cloud Zuul 通过与 Spring Cloud Eureka 的整合,实现了对服务实例的自动化维护,所以在使用服务路由配置的时候,我们不需要像传统路由配置方式那么为serviceId 指定具体的服务实例地址,只需要通过 zuul.routes..path 与 zuul.routes..serviceId 参数对的方式进行配置。

zuul:
  routes:
    # 面向服务的路由配置
    api-b:
      path: /api-b/**
      serviceId: feign-consumer

  对于面向服务的路由配置,除了使用 path 与 serviceId 映射的配置方式之外,还有一种更简洁的配置方式:zuul.routes.= ,其中 用来指定路由的具体服务名, 用来配置匹配的请求表达式。下面的路由规则等价于上面的path 与 serviceId 组合使用的配置方式:

zuul:
  routes:
    # 面向服务的路由配置
    feign-consumer: /api-c/**

  之所以能够通过serviceId 来解析转发具体的服务,是因为整合了 Eureka ,我们可以将 API 网关也看作 Eureka 服务治理下的一个普通的微服务应用。它除了会将自己注册到 Eureka 服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。所以在 Eureka 的帮助下, API 网关服务本身就以及维护了系统中所有 serviceId 与实例地址的映射关系。当有外部请求到达时,找到最佳匹配的path规则, API 网关就自动摇奖该请求路由到具体的 serviceId上,只需要通过Ribbon 的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作。

服务路由的默认规则

  虽然通过 Eureka 与 Zuul 的整合已经为我们省去了维护服务实例清单的大量匹配工作,我们只需要再维护请求路径的匹配表达式与服务名的映射关系即可。但是在实际操作中,往往大部分的配置规则几乎都会采用服务名作为外部请求的前缀,比如下面的配置:

zuul:
  routes:
    # 面向服务的路由配置
    hello-service:
        path: /hello-service/**
        serviceId: hello-service

  其中 path 路径的前缀使用了 hello-service, 而对应的服务名称也是 hello-service。
对此,我们希望可以自动化的完成类似上面的配置。好在,Zuul 默认进行了实现,当我们构建的 API 网关服务引入 Spring Cloud Eureka 之后,它为 Eureka 中的每个服务都自动创建一个默认路由规则,这些规则的 path 会使用 serviceId 配置的服务名作为请求前缀。
  由于默认情况下所有 Eureka 上的服务都会被 Zuul 自动地创建映射关系来进行路由,这会使得一些我们不希望对外开放的服务也可能被外部访问到。这是可以使用 zuul.ignored-services 参数来设置一个服务名匹配表达式定义不需要自动创建路由的规则,如果该值为 * ,则不会创建任何路由规则。

zuul:
  ignored-services: hello-service # 不会创建 hello-service 的默认路由规则
自定义路由映射规则

  我们在构建微服务系统进行业务逻辑开发的时候,为了兼容外部不同版本的客户端程序(尽量不强迫用户升级客户端),一般会采用开闭原则来进行设计和开发。这使得系统在迭代过程中,有时候会需要我们为一组互相配合的微服务定义一个版本标识来方便管理他们的版本关系,根据这个标识我们可以很容易地知道这些服务需要一起启动并配合使用。
  比如可以采用这样的命名:userService-v1、userService-v2。这样的在默认路由配置时,Zuul 自动为服务创建的路由表达式会采用服务名作为前缀,比如: /userService-v1 ,这样的路由来映射,但是这样生成处理的表达式规则较为单一,不利于通过路径来进行管理。通常的做法是为这些不同版本的微服务应用生成以版本代号作为路由前缀定义的路由规则,比如:/v1/userService/ 。这样的 URL 路径,我们就可以很容易的通过路径表达式来归类和管理这些具有版本信息的微服务了。
  针对上面的需求,如果我们的各个微服务应用都遵循了类似 userService-v1 这样的命名规则,通过-分隔的规范,那么,我们可以使用 Zuul 中自定义服务与路由映射关系的功能,来实现为符合上述规则的微服务自动化地创建类似 /v1/userService/** 的路由匹配规则。实现非常简单,只需在API网关程序中,添加如下的 Bean 即可:

// 自定义路由映射规则
@Bean
public PatternServiceRouteMapper serviceRouteMapper() {
   // 将 user-v1  -> v1/user
   return new PatternServiceRouteMapper("(?^.+)-(?v.+$)", "${version}/${name}");
}

  PatternServiceRouteMapper 对象可以让开发者通过正则表达式来自定义服务与路由映射的生成关系。其中构造方法的第一个参数是用来匹配服务名称是否符合该自定义规则的正则表达式,而第二个参数时定义根据服务名中定义的内容转换出的路径表达式规则。当开发者在 API 网关中定义了 PatternServiceRouteMapper 实现之后,只要符合第一个参数定义规则的服务名,都会优先使用该实现构建出的路径表达式,如果不符合,则还是会使用默认的路由映射规则(完整服务名)。
  我们添加上述Bean,修改feign-consumer 的 application name 为 feign-consumer-v1,启动服务,访问:localhost:5555/v1/feign-consumer/feign-consumer2,成功由zuul进行了路由,结果如下:
在这里插入图片描述

路径匹配

  不论是使用传统路由的配置方式还是服务路由的配置方式,我们都需要为每个路由规则定义匹配表达式,也就是上面所说的 path 参数。在Zuul中,路由匹配的路径表达式采用了Ant风格定义,它一共有下面三种通配符。

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

  另外,当我们使用通配符的时候,经常会碰到这样问题:一个URL路径可能会被多个不同路由的表达式匹配上。此时我们查看路由匹配算法中,它在使用的路由规则匹配请求的时候是通过线性遍历的方式,在请求路径获取到第一个匹配的路由规则之后就返回并结束匹配过程。所以当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序。
源码中-匹配到直接返回:

protected ZuulRoute getZuulRoute(String adjustedPath) {
   if (!matchesIgnoredPatterns(adjustedPath)) {
      for (Entry entry : getRoutesMap().entrySet()) {
         String pattern = entry.getKey();
         log.debug("Matching pattern:" + pattern);
         if (this.pathMatcher.match(pattern, adjustedPath)) {
            return entry.getValue();
         }
      }
   }
   return null;
}

保存的容器是 LinkedHashMap:

/**
* Map of route names to properties.
*/
private Map routes = new LinkedHashMap<>();

所以,路由规则的保存是有序的,而内容的加载是通过遍历配置文件中的路由规则一次加入,因此我们在编写配置文件时要注意顺序,但properties 的配置文件无法保证有序,此时需要使用 yaml 文件来配置,以实现有序的路由规则,比如使用以下的定义:

zuul:
  routes:
    # 面向服务的路由配置
      user-service-ext:
        path: /user-service/ext/**
        serviceId: user-service-ext
      user-service:
        path: /user-service/**
        serviceId: user-service
忽略表达式

  Zuul 还提供了一个忽略表达式参数 zuul.ignored-patterns。该参数可以用来设置不希望被 API 网关进行路由的 URL 表达式。

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

此时,只要访问的路径符合上面的表达式,就不会被正确的路由,控制台提示没有匹配的路由。该参数使用是需要注意它的范围并不是对某个路由,而是对所有路由都有效。

路由前缀

  Zuul 提供了 zuul.prefix 参数来进行设置,为全局路由规则增加前缀信息。比如希望为网关上的路由规则都增加 /api 前缀,那么我们可以在配置文件中增加配置:zuul.prefix=/api。另外,对于代理前缀会默认从路径中移除[转发时是否需要前缀,默认为不需要,会将前缀剥离],我们可以通过设置zuul.stripPrefix=false 来关闭该移除代理前缀的工作,也可以通过 zuul。routes..strip-prefix=true 来对指定路由关闭移除代理前缀的动作。
  另外, zuul.prefix 参数与路由表达式的起始字符串相同导致匹配失败的bug已经被修复。

zuul:
  prefix: /api
  strip-prefix: false
  routes:
    api-a:
      path: /api-a/**
      serviceId: hello-service
    api-b:
      path: /api-b/**
      serviceId: hello-service
    api-c:
      path: /ccc/**
      serviceId: hello-service
本地跳转

  Zuul 实现的 API 网关中,支持 forward 形式的服务端跳转配置,只需通过使用 path 与 url 的配置方式就能完成,通过 url 中使用 forward 来指定需要跳转的服务器资源路径。

zuul:
  routes:
    api-a:
      path: /api-a/**
      url: http://localhost:8001/
    api-b:
      path: /api-b/**
      url: forward:/local

  上面的配置实现了将符合 /api-b/** 规则的请求转发到 API 网关中以 /local 为前缀的请求上,由 API 网关进行本地处理。比如,当 API 网关接收到请求 /api-b/hello,它将符合 api-b 的路由规则,所以该请求会被 API 网关转发到网关的 /local/hello 请求上进行本地处理。由于需要在API网关上实现本地跳转,所以相应的我们也需要为本地跳转实现对应的请求接口,否则返回404错误。

/**
*
* describe zuul 实现 forward 跳转
* @author xmc
* @date 2019/1/25 10:29
* @param  * @param null
* @return
*/
@RestController
@RequestMapping("local")
public class HelloController {

   @RequestMapping("hello")
   public String hello() {
      return "Hello World From Local";
   }
}

Cookie 与头信息

  在默认情况下,Spring Cloud Zuul 在请求路由时,会过滤掉HTTP请求头信息中的一些敏感信息,防止它们被传递到下游的外部服务器。默认的敏感头信息通过zuul.sensitiveHeaders 参数定义,包括Cookie、Set-Cookie、Authorization 三个属性。这样会引发一个问题,如果我们将使用了Spring Security、Shiro 等安全框架构建的 Web 应用通过 Spring Cloud Zuul 构建的网关来进行路由时,Cookie 信息无法传递,会导致无法实现 登录和鉴权。

  • 通过设置全局参数为空来覆盖默认,不推荐,破坏了默认设置的用意
zuul.sensitiveHeaders=
  • 通过指定路由的参数来设置,仅对指定的web应用开启敏感信息传递
# 对指定路由开启自定义敏感头
zuul.routes..customSensitiveHeaders=true
# 将指定路由的敏感头信息设置为空
zuul.routes..sensitiveHeaders=
重定向问题

  解决了Cookie后,登录成功之后,我们跳转的页面 URL 却是具体 Web 应用实例的地址,而不是通过网关路由地址。无视了网关作为统一的入口。通过浏览器查看请求详情,引起问题的大致原因就是Spring Security 或 Shiro 在登录完成之后,通过重定向的方式跳转到登录后的页面,状态码为302,请求头信息中的 Host 指向了具体的服务实例IP地址和端口,该问题的根本原因在于 Spring Cloud Zuul 在路由请求时,并没有将最初的Host信息设置正确。
通过设置Zuul参数,可以将Host设置为最初的服务端请求地址:

zuul.addHostHeader=true
Hystrix 和 Ribbon 支持

  引入 Zuul 依赖时,其自动引入了 Ribbon、Hystrix 依赖,所以 Zuul 天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用客户端的负载均衡功能。但是如果使用的是 path 和 url 的映射关系来配置路由规则的时候,对于路由转发的请求不会采用 HystrxiCommand 来包装,所以这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此,尽量使用 path 和 serviceId 的组合来进行配置,不仅可以保证 API 网关的健壮和稳定,也能用到 Ribbon 的客户端负载均衡功能。
  我们在使用 Zuul 搭建网关的时候,可以通过Hystrix 和 Ribbon 的参数来调整路由请求的各种超时时间等配置(参考 Ribbon 和 Hystrix 配置),比如下面的这些配置:

  • hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 该参数用来设置 API 网关中路由转发请求的 HystrixCommand 执行超时时间。当路由转发请求的命令执行时间超过该配置值之后, Hystrix 会将执行的命令标记为 TIMEOUT,并抛出异常,Zuul 会对该异常进行处理并返回JSON 信息给调用方。
  • ribbon.ConnectTimeout: 该参数用来设置路由转发请求的时候,创建请求连接的超时时间。如果该时间小于上面的hystrix超时时间,会自动进行路由重试,如果依旧失败, Zuul会返回JSON信息给外部调用方。
  • ribbon.ReadTimeout: 该参数用来设置路由转发请求的超时时间,与上面连接时间类似,只是它的超时是对请求连接建立之后的处理时间。当此时间小于hystrix超时时间时,若路由请求的处理时间超过该配置时间且依赖服务的请求还未响应,会自动进行路由重试。

  在使用Zuul的服务路由时,如果路由转发请求发生超时(连接或处理超时),只要超时时间的设置小于Hystrix的命令超时时间,那么它会自动发起重试。
如果我们需要关闭重试,可以通过下面的方式进行配置:

zuul:
 retryable: false # 全局关闭重试
 routes:
   user-service:
     retryable: false # 指定路由关闭重试机制
     path: /user/**
     serviceId: user-service
   ```

你可能感兴趣的:(Spring,Cloud,个人记录)