zuul学习二:zuul路由详解(一)

0.836字数 4226阅读 10664

传统路由配置

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

没有Eureka等服务治理框架的帮助,我们需要根据服务实例的数量采用不同方式的配置来实现路由规则:

单实例配置:

通过zuul.routes.<route>.pathzuul.routes.<route>.url参数对的方式进行配置,比如:

zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.url=http://localhost:8080/

如果一个请求http://localhost:6069/user-service/hello就被请求转发到http://localhost:8080/hello地址。

demo
启动一个简单的web服务,不注册eureka,访问路径

http://localhost:8080/user/index

启动zuul服务

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
     defaultZone: http://localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    prefer-ip-address: true
server:
  port: 6069
zuul:
  routes:
    user-service:
      path: /user-service/**
      url: http://localhost:8080/

代理之后可以访问:

http://localhost:6069/user-service/user/index

多实例配置:

通过zuul.routes.<route>.pathzuul.routes.<route>.serviceId参数的方式进行配置,比如:

zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
ribbon.eureka.enabled=false
user-service.ribbon.listOfServers=http://localhost:8080/,http://localhost:8081/

当请求为求http://localhost:6069/user-service/就被转发到http://localhost:8080/,http://localhost:8081/两个实例地址上了。serviceId用户手工命名的服务名称,配合ribbon.listOfServers参数实现服务与实例的维护。由于存在多个实例,api网关在进行路由转发需要实现负载均衡策略,这里还需要spring cloud ribbon配合。由于spring cloud zuul中自带了ribbon的依赖。

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

demo
启动user服务(不注册到eureka),使用8080和8081接口

http://localhost:8080/user/index
http://localhost:8081/user/index

启动zuul,zuul项目的配置如下:

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
     defaultZone: http://localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    prefer-ip-address: true
server:
  port: 6069

zuul:
  routes:
    users:
      path: /user-service/**
      serviceId: user-service

ribbon:
  eureka:
    enabled: false

user-service:
  ribbon:
    listOfServers: http://localhost:8080/,http://localhost:8081/

访问:

http://localhost:6069/user-service/user/index

通过日志看出的确是负载均衡了。

注意
These simple url-routes don’t get executed as a HystrixCommand
nor can you loadbalance multiple URLs with Ribbon. To achieve this, specify a service-route and configure a Ribbon client for the serviceId (this currently requires disabling Eureka support in Ribbon: see above for more information), e.g.
这些使用简单的url-routes不能够使用HystrixCommand并且不能使用Ribbon对多URL实现路由。如果想去使用这一点,特定的指定服务理并且配置ribbon去负载均衡:可以查看above for more information

参考资料
官网Embedded Zuul Reverse Proxy

服务路由配置

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

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

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

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

zuul巧妙的整合了Eureka来实现面向服务的路由。实际上,我们可以直接将api网关也看作Eureka服务治理下的一个普通的微服务应用。它除了会将自己注册到Eureka服务注册中心上之外,也会从注册中心获取所有服务以及他们的实例清单。在Eureka的帮助下,api网关服务本身就已经维护了系统中所有serviceId与实例地址的映射关系。当有外部请求到达api网关的时候,根据请求的url路径找到最佳匹配的path,api网关就可以知道要将请求路由到哪个具体的serviceId上去。由于api网关中已经知道serviceId对应服务实例的地址清单,那么只需要通过ribbon的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作了。

服务路由的默认规则

虽然通过Eurekazuul的整合已经为我们省去了维护服务实例清单的大量配置工作,剩下来只需要再维护请求路径的匹配表达式与服务名映射关系即可。

但是实际的运用过程中发现,大部分的路由规则机会都会采用服务名作为外部请求的前缀,比如下面的列子,其中path路径的前缀使用了user-service,而对应的服务名也是user-service

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

其实zuul已经自动的帮我们实现以服务名作为前缀的映射,我们不需要去配置它。

但是,有一些服务我们不需要对外开发也被外部访问到了。这个时候我们可以使用zuul.ignore-services参数来设置一个服务名匹配表达式来定义不自动创建路由的规则。zuul在自动创建服务路由的时候会根据该表达式来进行判断,如果服务名匹配表达式,那么zuul将跳过该服务,不为其创建路由规则。比如,设置为zuul.ignored-services=*的时候,zuul将对所有的服务都不自动创建路由规则。在这种情况下,我们就要在配置文件中为需要路由的服务添加路由规则(可以使用pathserviceId组合的配置方式,也可以使用更简洁的zuul.routes.<serviceId>=<path>配置方式),只有在配置文件中出现的映射规则会被创建路由,而从Eureka中获取的其他服务,zuul将不会为他们创建路由规则。

之前的博客讲过,可以参考下面的博客:
zuul学习一:spring cloud zuul的快速入门
官网Embedded Zuul Reverse Proxy

自定义路由映射关系

我们在构建微服服务系统的进行业务逻辑开发的时候,为了兼容外部不同版本的客户端程序(尽量不强迫用户升级客户端),一般都会采用开闭原则来进行设计与开发。这使得系统在迭代过程中,有时候需要我们为一组互相配合的微服务定义一个版本标记来方便管理它们的版本关系,根据这个标记我们可以很容易的知道这些服务需要一起启动并配合使用。比如:userservice-v1,userservice-v2,orderservice-v1,orderservice-v2等等。默认情况下,zuul自动为服务创建的路由表达式会采用服务名作为前缀,比如针对上面的userservice-v1userservice-v2,它会产生/userservice-v1/userservice-v2两个路径表达式来映射,这样生成出来的表示式规则单一,不利于管理。通常的做法就是为这些不同的版本的微服务应用生成以版本号作为路由前缀定义规则的路由规则,比如/v1/userservice/。这时候,通过这样具有版本号前缀的url路径,我们就可以很同意的通过路径表达式来归类和管理这些具有版本信息的微服务了。

我们可以使用zuul中自定义服务与路由映射关系的功能,创建类似于/v1/userserivce/**的路由匹配原则。

@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
        return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
}

PatternServiceRouteMapper对象可以让开发者通过正则表达式来自定义服务与路由映射的生成关系。构造函数第一个参数是用来匹配服务名称是否符合该自定义规则的正则表达式,第二个参数是定义根据服务名中定义的内容转换出的路径表达式规则。当开发者在api网关中定义了PatternServiceRouteMapper实现之后,只需符合第一个参数定义规则的服务名,都会优先使用该实现构建出的表达式,如果没有匹配上的服务规则则还是会使用默认的路由映射规则,记采用完整服务名作为前缀的路径表达式。

demo
定义users服务,注册到eureka上为users-v1,符合上述的服务名称将版本号,启动eureka和users服务

访问user服务

http://192.168.1.57:8080/user/index

启动zuul:

@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class);
    }

    @Bean
    public PatternServiceRouteMapper serviceRouteMapper(){
        return new PatternServiceRouteMapper("(?<name>^.+)-(?<version>v.+$)","${version}/${name}");
    }
}

配置文件:

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
     defaultZone: http://localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    prefer-ip-address: true
server:
  port: 6069

访问:

http://192.168.1.57:6069/v1/users/user/index

访问的方式符合我们说的版本号/服务名/

参考资料
官网Embedded Zuul Reverse Proxy

路径匹配

不论是使用传统配置方式还是服务路由的配置方式,我们都需要为每个路由定义匹配表达式,也就是上面的oath参数,在zuul中,路由匹配的路径表达式采用ant风格定义。

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

ant风格的路径表达式使用起来非常简单,

通配符 说明
? 匹配任意单个字符
* 匹配任意数量的字符
** 匹配任意数量的自负,支持多级目录
url路径 说明
/user-service/? 可以匹配/user-service/之后的一个人和字符的路径,比如/user-service/a,/user-service/b,/user-service/c
/user-service/* 可以匹配/user-service/之后拼接的任意字符的路径,比如说/user-service/a,/user-service/aaa,无法匹配/user-service/a/b
/user-service/** 可以匹配/user-service/*包含的内容之外,还可以匹配/user-service/a/b的多级目录

但是随着版本的迭代,对user-service服务做了一些功能拆分,将原本属于user-service服务的某些功能拆分到user-service-ext中去,而这些拆分的外部调用url路径希望能够复合/user-service/ext/**。这个时候

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

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

此时,调用user-service-ext服务的url路径实际上会同时被/user-service/**/user-service/ext/**两个表示式所匹配。在逻辑上,api网关优先选择/user-service/ext/**路由,然后再去匹配/user-service/**路由才能实现上述需求,但是如果使用上面的配置方式,实际上是无法保证这样的路由优先顺序的。

从下面的路由匹配算法中,我们可以看到它在使用路由规则匹配的请求路径的时候是通过线性便利的方法,在请求路径获取到第一个匹配的路由规则之后就返回并结束匹配过程。所以当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序。

org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator

由于properties的配置内容无法保证有序,所以当出现这样的情况的时候,为了保证路由的优先顺序,我们需要使用yml文件来配置,以实现有序的路由规则

zuul:
  routes: 
    user-service-ext: 
      path: /user-service/ext/**
      serviceId: user-service-ext
    user-service:
      path: /user-service/**
      serviceId: user-service

关于这边的说法,官网给的介绍

If you need your routes to have their order preserved you need to use a YAML file as the ordering will be lost using a properties file.

忽略表达式

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

比如我们启动user-service服务,访问

http://192.168.1.57:6069/user/home

可以使用api网关路由

http://192.168.1.57:6069/user-service/user/home

在zuul-service中配置

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
     defaultZone: http://localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    prefer-ip-address: true
server:
  port: 6069
zuul:
  ignoredPatterns: /**/home/**
  routes:
    user-service:
      path: /user-service/**
      serviceId: user-service
    order-service:
      path: /pay-service/**
      serviceId:  pay-service
logging:
  level:
    com.netflix: debug

发现url中包括home的已经不能被正确路由了。zuul.ignoredPatterns=//home/ 的使用方法。

控制台上输出:


另外,该参数在使用时还需要注意它的范围并不是针对某个路由,而是对所有路由。所以在设置的时候需要全面考虑url规则,防止忽略了不该被忽略的url路径。

参考资料
官网Embedded Zuul Reverse Proxy

路由前缀

为了方便地为路由规则增加前缀信息,zuul提供了zuul.prefix参数来进行设置。比如,希望为网关上的路由规则增加/api前缀,那么我们可以在配置文件中增加配置:zuul.prefix=/api。另外,对于代理前缀会默认从路径中移除,我们可以通过设置zuul.strip-prefix=false(默认为true,默认为true时前缀生效,比如http://192.168.5.3:6069/zhihao/users/user/index)来关闭该移除代理前缀的动作。

demo
启动user和order服务

http://192.168.1.57:8080/user/index
http://192.168.1.57:9090/order/index

启动zuul服务,zuul中配置了前缀

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
     defaultZone: http://localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    prefer-ip-address: true
server:
  port: 6069
zuul:
  routes:
    user-service:
      path: /user-service/**
      serviceId: user-service
    order-service:
      path: /pay-service/**
      serviceId:  pay-service
  prefix: /zhihao
http://192.168.1.57:6069/zhihao/order-service/order/index
http://192.168.1.57:6069/zhihao/user-service/user/index

stripPrefix的使用
修改zuul的配置:

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
     defaultZone: http://localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    prefer-ip-address: true
server:
  port: 6069
zuul:
  routes:
    user-service:
      path: /user-service/**
      serviceId: user-service
    order-service:
      path: /pay-service/**
      serviceId:  pay-service
  prefix: /zhihao
  strip-prefix: false
logging:
  level:
    com.netflix: debug

增加了zuul.strip-prefix: false和配置了zuul的日志级别。
再去访问之前的代理:

http://192.168.1.57:6069/zhihao/user-service/user/index

发现访问不了,控制台上打印出日志,访问到user服务的/zhihao/user/index了

2017-08-12 16:50:11.656 DEBUG 1407 --- [nio-6069-exec-8] c.n.loadbalancer.LoadBalancerContext     
: user-service using LB returned Server: 192.168.1.57:8080 for request /zhihao/user/index

此时正确的姿势是在user服务中增加server.context-path=/zhihao,再去访问就正确了:

http://192.168.1.57:6069/zhihao/user-service/user/index

同时order服务也访问不了,也要在order服务加server.context-path=/zhihao,其实zuul.prefix=/zhihaozuul.strip-prefix=false表示所有的服务都要跳过服务配置在真实请求求加上/zhihao。

也有针对当个服务的
zuul服务的配置文件:

spring:
  application:
    name: zuul-service
eureka:
  client:
    service-url:
     defaultZone: http://localhost:8761/eureka
  instance:
    instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    prefer-ip-address: true
server:
  port: 6069
zuul:
  routes:
    user-service:
      path: /user-service/**
      stripPrefix: false
      serviceId: user-service
    order-service:
      path: /pay-service/**
      serviceId:  pay-service
  prefix: /zhihao
logging:
  level:
    com.netflix: debug
http://192.168.1.57:6069/zhihao/order-service/order/index
http://192.168.1.57:6069/zhihao/user-service/user/index

前者后者都没有配置context-path,前者能够访问,后者不能访问,发现因为后者配置了zuul.routes.user-service.stripPrefix=false,发现后者真正访问到的的服务是/user-service/user/index,此时要修改user-service增加server.context-path=/user-service才能正确地访问到服务。


user-service的配置改成下面这样

spring:
application:
  name: zuul-service
eureka:
client:
  service-url:
   defaultZone: http://localhost:8761/eureka
instance:
  instance-id:  ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
  prefer-ip-address: true
server:
port: 6069
zuul:
routes:
  user-service:
    path: /zhihao-userservice/**
    serviceId: user-service
  order-service:
    path: /pay-service/**
    serviceId:  pay-service
prefix: /zhihao
logging:
level:
  com.netflix: debug

http://192.168.1.57:6069/zhihao/order-service/order/index还是正确路由,
http://192.168.1.57:6069/zhihao/zhihao-userservice/user/index不能正确路由。

这时zuul的一个bug,当路由表达式前缀是以zhihao开头,与路由表达式一样是/zhihao开头的话就会产生错误的映射关系。

我使用的是Camden.SR7版本也存在这样的问题。

参考资料
官网Embedded Zuul Reverse Proxy

本地跳转

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

在zuul-service服务中定义一个controller,

@RestController
public class HelloController {

   @RequestMapping("/local/hello")
   public String hello(){
       return "hello world local";
   }
}

配置文件配置:

zuul:
  routes:
    user-service:
      path: /zhihao-userservice/users/**
      serviceId: user-service
    pay-service:
      path: /pays/**
      serviceId: pay-service
    zuul-service:
      path: /api-b/**
      serviceId: forward:/local

访问http://192.168.5.3:6069/api-b/hello就跳转到了网关的/local/hello上了。

cookie与头信息

默认情况下,spring cloud zuul在请求路由时,会过滤掉http请求头信息中一些敏感信息,防止它们被传递到下游的外部服务器。默认的敏感头信息通过zuul.sensitiveHeaders参数定义,默认包括cookie,set-Cookie,authorization三个属性。所以,我们在开发web项目时常用的cookie在spring cloud zuul网关中默认时不传递的,这就会引发一个常见的问题,如果我们要将使用了spring securityshiro等安全框架构建的web应用通过spring cloud zuul构建的网关来进行路由时,由于cookie信息无法传递,我们的web应用将无法实现登录和鉴权。为了解决这个问题,配置的方法有很多。

  • 通过设置全局参数为空来覆盖默认值,具体如下:
zuul.sensitiveHeaders=

这种方法不推荐,虽然可以实现cookie的传递,但是破坏了默认设置的用意。在微服务架构的api网关之内,对于无状态的restful api请求肯定时要远多于这些web类应用请求的,甚至还有一些架构设计会将web类应用和app客户端一样归为api网关之外的客户端应用。

  • 通过指定路由的参数来设置,方法有下面二种。
    方法一:对指定路由开启自定义敏感头。
    方法二:将指定路由的敏感头设置为空。
 zuul:
  routes:
    users:
      path: /myusers/**
      sensitiveHeaders: 
      url: https://downstream

将具体的服务的sensitiveHeaders(头信息设置为空)

比较推荐使用这二种方法,仅对指定的web应用开启对敏感信息的传递,影响范围小,不至于引起其他服务的信息泄露问题。

参考资料
Zuul Http Client
Cookies and Sensitive Headers
The Routes Endpoint
Spring Cloud实战小贴士:Zuul处理Cookie和重定向

本博客代码
代码地址

推荐阅读更多精彩内容