资料参考:《Spring Cloud 微服务实战》
目录
路由详解
传统路由配置
所谓的传统路由配置方式就是在不依赖于服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现API网关对外部请求的路由。
没有Eureka和Consul的服务治理框架帮助的时候,我们需要根据服务实例的数量采用不同方式的配置来实现路由规则:
- 单实例配置:通过一组
zuul.routes.<route>.path
与zuul.routes.<route>.url
参数对的方式配置,比如:
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.url=http://localhost:8080/
该配置实现了对符合/user-service/**
规则的请求路径转发到http://localhost:8080/
地址的路由规则,比如,当有一个请求http://localhost:1101/user-service/hello
被发送到API网关上,由于/user-service/hello
能够被上述配置的path
规则匹配,所以API网关会转发请求到http://localhost:8080/hello
地址。
- 多实例配置:通过一组
zuul.routes.<route>.path
与zuul.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/
该配置实现了对符合/user-service/**
规则的请求路径转发到http://localhost:8080/
和http://localhost:8081/
两个实例地址的路由规则。它的配置方式与服务路由的配置方式一样,都采用了zuul.routes.<route>.path
与zuul.routes.<route>.serviceId
参数对的映射方式,只是这里的serviceId
是由用户手工命名的服务名称,配合<serviceId>.ribbon.listOfServers
参数实现服务与实例的维护。由于存在多个实例,API网关在进行路由转发时需要实现负载均衡策略,于是这里还需要Spring Cloud Ribbon的配合。由于在Spring Cloud Zuul中自带了对Ribbon的依赖,所以我们只需要做一些配置即可,比如上面示例中关于Ribbon的各个配置,它们的具体作用如下:
ribbon.eureka.enabled
:由于zuul.routes.<route>.serviceId
指定的是服务名称,默认情况下Ribbon会根据服务发现机制来获取配置服务名对应的实例清单。但是,该示例并没有整合类似Eureka之类的服务治理框架,所以需要将该参数设置为false,不然配置的serviceId
是获取不到对应实例清单的。user-service.ribbon.listOfServers
:该参数内容与zuul.routes.<route>.serviceId
的配置相对应,开头的user-service
对应了serviceId
的值,这两个参数的配置相当于在该应用内部手工维护了服务与实例的对应关系。
不论是单实例还是多实例的配置方式,我们都需要为每一对映射关系指定一个名称,也就是上面配置中的<route>
,每一个<route>
就对应了一条路由规则。每条路由规则都需要通过path
属性来定义一个用来匹配客户端请求的路径表达式,并通过url
或serviceId
属性来指定请求表达式映射具体实例地址或服务名。
服务路由配置
服务路由我们在上一篇中也已经有过基础的介绍和体验,Spring Cloud Zuul通过与Spring Cloud Eureka的整合,实现了对服务实例的自动化维护,所以在使用服务路由配置的时候,我们不需要向传统路由配置方式那样为serviceId
去指定具体的服务实例地址,只需要通过一组zuul.routes.<route>.path
与zuul.routes.<route>.serviceId
参数对的方式配置即可。
比如下面的示例,它实现了对符合/user-service/**
规则的请求路径转发到名为user-service
的服务实例上去的路由规则。其中<route>
可以指定为任意的路由名称。
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
对于面向服务的路由配置,除了使用path
与serviceId
映射的配置方式之外,还有一种更简洁的配置方式:zuul.routes.<serviceId>=<path>
,其中<serviceId>
用来指定路由的具体服务名,<path>
用来配置匹配的请求表达式。比如下面的例子,它的路由规则等价于上面通过path
与serviceId
组合使用的配置方式。
zuul.routes.user-service=/user-service/**
传统路由的映射方式比较直观且容易理解,API网关直接根据请求的URL路径找到最匹配的path
表达式,直接转发给该表达式对应的url
或对应serviceId
下配置的实例地址,以实现外部请求的路由。那么当采用path
与serviceId
以服务路由方式实现时候,没有配置任何实例地址的情况下,外部请求经过API网关的时候,它是如何被解析并转发到服务具体实例的呢?
在Spring Cloud Netflix中,Zuul巧妙的整合了Eureka来实现面向服务的路由。实际上,我们可以直接将API网关也看做是Eureka服务治理下的一个普通微服务应用。它除了会将自己注册到Eureka服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。所以,在Eureka的帮助下,API网关服务本身就已经维护了系统中所有serviceId与实例地址的映射关系。当有外部请求到达API网关的时候,根据请求的URL路径找到最佳匹配的path
规则,API网关就可以知道要将该请求路由到哪个具体的serviceId
上去。由于在API网关中已经知道serviceId
对应服务实例的地址清单,那么只需要通过Ribbon的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作了。
服务路由的默认规则
虽然通过Eureka
与zuul
的整合已经为我们省去了维护服务实例清单的大量配置工作,剩下来只需要再维护请求路径的匹配表达式与服务名映射关系即可。
但是实际的运用过程中发现,大部分的路由规则机会都会采用服务名作为外部请求的前缀,比如下面的列子,其中path路径的前缀使用了user-service
,而对应的服务名也是user-service
。
zuul.routes.user-service.path=/user-service/**
zuul.routes.user-service.serviceId=user-service
其实zuul已经自动的帮我们实现以服务名作为前缀的映射,我们不需要去配置它。
当我们为Zuul构建API网关服务引入Eureka的时候,它为Eureka中每个服务都自动创建一个默认路由规则,这些默认规则的path会使用serviceId配置的服务名作为前缀。
但是,有一些服务我们不需要对外开发也被外部访问到了。这个时候我们可以使用zuul.ignore-services
参数来设置一个服务名匹配表达式来定义不自动创建路由的规则。zuul
在自动创建服务路由的时候会根据该表达式来进行判断,如果服务名匹配表达式,那么zuul
将跳过该服务,不为其创建路由规则。比如,设置为zuul.ignored-services=*
的时候,zuul
将对所有的服务都不自动创建路由规则。在这种情况下,我们就要在配置文件中为需要路由的服务添加路由规则(可以使用path
与serviceId
组合的配置方式,也可以使用更简洁的zuul.routes.<serviceId>=<path>
配置方式),只有在配置文件中出现的映射规则会被创建路由,而从Eureka
中获取的其他服务,zuul
将不会为他们创建路由规则。
自定义路由映射关系
我们在构建微服服务系统的进行业务逻辑开发的时候,为了兼容外部不同版本的客户端程序(尽量不强迫用户升级客户端),一般都会采用开闭原则来进行设计与开发。这使得系统在迭代过程中,有时候需要我们为一组互相配合的微服务定义一个版本标记来方便管理它们的版本关系,根据这个标记我们可以很容易的知道这些服务需要一起启动并配合使用。比如:userservice-v1
,userservice-v2
,orderservice-v1
,orderservice-v2
等等。默认情况下,zuul自动为服务创建的路由表达式会采用服务名作为前缀,比如针对上面的userservice-v1
和userservice-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
实现之后,只需符合第一个参数定义规则的服务名,都会优先使用该实现构建出的表达式,如果没有匹配上的服务规则则还是会使用默认的路由映射规则,记采用完整服务名作为前缀的路径表达式。
路径匹配
不论是使用传统配置方式还是服务路由的配置方式,我们都需要为每个路由定义匹配表达式,也就是上面的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/**
路由才能实现上述需求,但是如果使用上面的配置方式,实际上是无法保证这样的路由优先顺序的。
从下面的路由匹配算法中,我们可以看到它在使用路由规则匹配的请求路径的时候是通过线性便利的方法,在请求路径获取到第一个匹配的路由规则之后就返回并结束匹配过程。所以当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序。
由于properties的配置内容无法保证有序,所以当出现这样的情况的时候,为了保证路由的优先顺序,我们需要使用yml文件来配置,以实现有序的路由规则
关于这边的说法,官网给的介绍
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路径。
路由前缀
为了方便地为路由规则增加前缀信息,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=/zhihao
和zuul.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版本也存在这样的问题。
本地跳转
在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 security
,shiro
等安全框架构建的web应用通过spring cloud zuul
构建的网关来进行路由时,由于cookie信息无法传递,我们的web应用将无法实现登录和鉴权。为了解决这个问题,配置的方法有很多。
- 通过设置全局参数为空来覆盖默认值,具体如下:
zuul.sensitiveHeaders=
这种方法不推荐,虽然可以实现cookie的传递,但是破坏了默认设置的用意。在微服务架构的api网关之内,对于无状态的restful api请求肯定时要远多于这些web类应用请求的,甚至还有一些架构设计会将web类应用和app客户端一样归为api网关之外的客户端应用。
- 通过指定路由的参数来设置,方法有下面二种。
方法一:对指定路由开启自定义敏感头。
方法二:将指定路由的敏感头设置为空。
将具体的服务的sensitiveHeaders(头信息设置为空)
比较推荐使用这二种方法,仅对指定的web应用开启对敏感信息的传递,影响范围小,不至于引起其他服务的信息泄露问题。
参考文章:Spring Cloud实战小贴士:Zuul处理Cookie和重定向
重定向问题
在使用Spring Cloud Zuul对接Web网站的时候,处理完了会话控制问题之后。往往我们还会碰到如下图所示的问题,我们在浏览器中通过Zuul发起了登录请求,该请求会被路由到某WebSite服务,该服务在完成了登录处理之后,会进行重定向到某个主页或欢迎页面。此时,仔细的开发者会发现,在登录完成之后,我们浏览器中URL的HOST部分发生的改变,该地址变成了具体WebSite服务的地址了。这就是在这一节,我们将分析和解决的重定向问题!
出现该问题的根源是Spring Cloud Zuul没有正确的处理HTTP请求头信息中的Host导致。在Brixton版本中,Spring Cloud Zuul的PreDecorationFilter
过滤器实现时完全没有考虑这一问题,它更多的定位于REST API的网关。所以如果要在Brixton版本中增加这一特性就相对较为复杂,不过好在Camden版本之后,Spring Cloud Netflix 1.2.x版本的Zuul增强了该功能,我们只需要通过配置属性zuul.add-host-header=true
就能让原本有问题的重定向操作得到正确的处理。