传统路由配置就是不需要依赖服务发现机制,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现API网关对外请求的路由。
单实例配置
通过zuul.routes.
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.url= http://localhost:2222/
该配置将符合/api-a/**规则的请求路径转发到http://localhost:2222/地址的路由规则。比如一个请求http://localhost:5111/api-a/hello被发送到API网关上,由于/api-a/hello能够被上述配置的path规则匹配,所以API网关会转发请求到http://localhost:2222/hello地址。
多实例配置
通过zuul.routes.
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.serviceId= hello-service
ribbon.eureka.enabled=false
hello-service.ribbon.listOfServers=http://localhost:2222/,http://localhost:2223/
该配置将符合/api-a/**规则的请求路径转发到http://localhost:2222/和http://localhost:2223/地址的路由规则。它的配置方式和下面介绍的服务路由的配置方式一样都要指定zuul.routes.
总结:不论是单实例还是多实例的配置方式,都需要为每一对映射关系指定一个名称,也就是
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.serviceId= hello-service
由于Zuul整合了Eureka,所以不需要像传统路由配置方式那样手动指定服务实例地址。上面的示例将符合/api-a/**规则的请求路径转发到hello-service的服务实例上的路由规则。它还有一种更简洁的写法:zuul.routes.
zuul.routes.hello-service= /api-a/**
通过Eureka和Zuul的整合我们省去了维护实例清单的大量配置工作,剩下的只需要再维护请求路径的匹配表达式与服务名映射关系即可。但是在实际的运用过程中会发现,大部分的路由配置规则几乎都会采用服务名作为外部请求的前缀,比如下面的例子,其中path路径的前缀使用了hello-service,而对应的服务名称也是hello-service。
zuul.routes.hello-service.path= /hello-service/**
zuul.routes.hello-service.serviceId= hello-service
对于这样具有规则性的配置内容,在Zuul和Eureka整合之后,它为Eureka中的每一个服务都自动创建一个默认路由规则,这些默认规则的path会使用serviceId配置的服务名作为请求前缀,就和上面的例子一样。
默认情况下,Zuul会为所有Eureka服务自动的创建映射关系来进行路由,这会使不希望对外开放的服务也可以被外部访问到,这时候可以使用zuul.ignored-services参数来设置一个服务名匹配表达式来定义不自动创建路由的规则。Zuul在自动创建服务路由时会根据该表达式判断,如果服务名匹配则跳过不创建默认路由规则。比如设置zuul.ignored-services=*则表示所有服务不自动创建默认路由规则,设置zuul.ignored-services=hello-service(多个逗号隔开)则表示hello-service服务不自动创建默认路由规则。
我们在构建微服务系统进行业务逻辑开发的时候,为了兼容外部不同版本的客户端程序(尽量不强迫用户升级客户端),一般会采用开闭原则来进行设计开发。这使得系统在迭代过程中,有时候会需要我们为一组互相配合的微服务定义一个版本标识来方便管理它们的版本关系,根据这个标识我们可以很容易知道哪些服务需要一起配合使用。
比如可以采用类似这样的命名:userservice-v1、userservice-v2、orderservice-v1、orderservice-v2。默认情况下,Zuul自动为服务创建的路由表达式会采用服务名做前缀,比如对应上面的映射为/userservice-v1、/userservice-v2、/orderservice-v1、/orderservice-v2,但是这样生成的表达式规则单一,不利于通过路径规则进行管理。通常的做法是生成版本号作为路由前缀的路由规则,比如:/v1/userservice、/v2/userservice。
针对这个需求,如果我们的各个微服务都遵循了类似userservice-v1这样的命名规则,那么我们可以使用Zuul中自定义服务于路由映射关系的功能,来实现自动化的创建类似/v1/userservice/**的路由匹配规则。只需要在API网关服务的应用主类中增加如下Bean的创建即可:
@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
return new PatternServiceRouteMapper(
"(?^.+)-(?v.+$)",
"${version}/${name}");
}
PatternServiceRouteMapper 对象可以通过曾泽表达式来自定义服务路由与映射的生成关系。其中构造方法的第一个参数是用来匹配服务名称是否符合该自定义规则的正则表达式,而第二个参数是定义根据服务名中定义的内容转换出的路径表达式规则。只要符合第一个参数定义规则的服务名都会优先使用该实现构建出的路径表达式,没有匹配的服务则按默认的路由规则映射。
注意:使用默认的路由规则就不需要在配置文件里配置那些映射关系了。
默认情况下,如果将@EnableZuulProxy
与Spring Boot Actuator一起使用,则可以启用另外两个端点:
路由端点
通过/actuator/routes的GET请求来返回映射路由的列表
通过/actuator/routes/details的GET请求来返回映射路由的详细列表
注意,我其实只显示的配置了/api-a/**和/api-b/**,但是这里多了/hello-service/**,/hello-service/**和/api-a/**是同一个服务的路由规则,为什么会出现这种情况,是因为Zuul整合了Eureka会给Eureka提供给它的实例生成默认路由规则,我显示的配置了/api-a/**和/api-b/**,但是/api-b/**的feign-consumer服务并没有启动,所以只给hello-service生成了默认的路由规则,可以通过上面说到的zuul.ignored-services=hello-service来忽略hello-service的默认路由规则配置。修改后的配置文件:
spring:
application:
name: api-gateway #为服务命名
server:
port: 5111
eureka:
client:
service-url:
defaultZone: http://localhost:1111/eureka/ #指定服务注册中心位置
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
#actuator设置
management:
endpoints:
web:
exposure:
include: "*" #暴露所有端点 默认是info和health
endpoint:
health:
show-details: always #默认是never
#服务路由配置
zuul:
ignored-services: hello-service
routes:
api-a:
path: /api-a/**
serviceId: hello-service
api-b:
path: /api-b/**
serviceId: feign-consumer
对/routes的POST强制刷新现有路由(例如,当服务目录中有更改时),可以通过将management.endpoints.routes.enabled设置为false来禁用此端点。
过滤端点
通过/actuator/filters的GET请求来返回Zuul过滤器的映射列表。
不论是使用传统路由的配置还是服务路由的配置都需要为每个路由规则定义匹配表达式,也就是path参数。在Zuul中,路由匹配的表达式采用了Ant风格定义,它有下面三种通配符。
通配符 | 说明 |
? | 匹配任意单个字符 |
* | 匹配任意数量的字符 |
** | 匹配任意数量的字符,支持多及目录 |
示例:
URL路径 | 说明 |
/hello-service/? | 它可以匹配/hello-service/之后拼接一个任意字符的路径。比如/hello-service/a、/hello-service/b |
/hello-service/* | 它可以匹配/hello-service/之后拼接任意字符的路径。比如/hello-service/a、/hello-service/aaa,无法匹配/hello-service/a/b。 |
/hello-service/** | 它可以匹配/hello-service/*包含的内容外,还可以匹配形如/hello-service/a/b这样的多级目录路径。 |
另外,当我们使用通配符的时候,可能遇到一个URL路径被多个不同的路由的表达式匹配上的情况。比如,我们一开始构建了hello-service服务,并且配置了如下路由规则。
zuul.routes.hello-service.path= /hello-service/**
zuul.routes.hello-service.serviceId= hello-service
随着版本的迭代,我们对hello-service功能做了拆分,将原属于hello-service服务的功能拆分到了另一个全新的服务hello-service-ext中,而这些拆分的外部调用URL路径希望能够符合规则/hello-service/ext/**,这个时候我们新加路由规则
zuul.routes.hello-service-ext.path= /hello-service/ext/**
zuul.routes.hello-service-ext.serviceId= hello-service-ext
此时调用hello-service-ext服务的URL路径会被 /hello-service/**和 /hello-service/ext/**所匹配。所以我们需要优先选择/hello-service/ext/**路由,然后再匹配/hello-service/**路由。在Zuul中,是通过线性遍历的方式,来判定请求路径与配置文件内配置的路由规则是否匹配,匹配就结束匹配过程,所以当存在多个匹配的路由规则时,匹配结果取决于路由规则的保存顺序。使用properties文件无法保证有序,所以要使用yml文件来配置,实现有序的路由规则。比如:
zuul:
routes:
hello-service-ext:
path: /hello-service/ext/**
serviceId: hello-service-ext
hello-service:
path: /hello-service/**
serviceId: hello-service
注意:使用application.properties文件会丢失路由规则的配置顺序,使用yml文件不会。
通过zuul.ignored-patterns参数可以用来设置不希望被API网关进行路由的URL表达式。比如如果不希望/hello接口被路由可以这么设置:
zuul.ignored-patterns=/**/hello/**
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.serviceId= hello-service
注意:该参数并不是对某个路由设置的,而是对所有路由有效。
Zuul提供了zuul.prefix参数来为全局的路由规则增加前缀信息。
比如希望为网关上的路由规则都增加/api前缀,那么我们可以在配置文件中增加配置:zuul.prefix=/api。另外关于代理前缀在请求转发时会默认从路径中移除,可以通过设置zuul.stripPrefix=false来关闭移除代理前缀的动作,也可以通过zuul.routes.
zuul:
prefix: /api
routes:
api-a:
path: /api-a/**
serviceId: hello-service
此时的请求路径应该为http://localhost:5111/api/api-a/hello。
当 zuul.stripPrefix=true 时 (http://127.0.0.1:8181/api/user/list -> http://192.168.1.100:8080/user/list),当 zuul.stripPrefix=false时(http://127.0.0.1:8181/api/user/list -> http://192.168.1.100:8080/api/user/list)
注意:此参数在Brixton版本和Camden版本中,如果配置的前缀和与路由的起始字符串相同则会有Bug,比如给/api-a/配置/api前缀就会有bug,这个bug在Finchley版本已经被修改。
Zuul支持以forward形式的服务跳转配置。
zuul.routes.api-a.path= /api-a/**
zuul.routes.api-a.url= http://localhost:2222/
zuul.routes.api-b.path= /api-b/**
zuul.routes.api-b.url= forward:/local
上面的示例中,会将符合/api-b/**的请求转发到API网关中以/local为前缀的请求上。比如http://localhost:5551/api-b/hello会被转发到http://localhost:5551/local/hello进行本地处理,但是你必须在本地提供/local/hello的接口,如下,否则会报404错误。
@RestController
public class HelloController {
/**
* 本地方法
* @return
*/
@GetMapping("/local/hello")
public String hello() {
return "local";
}
}
过滤敏感Headers
默认情况下,Spring Cloud Zuul在请求路由时,会过滤掉HTTP请求头信息中的一些敏感信息,防止它们被传递到下游的外部服务器。默认的敏感头信息通过zuul.sensitive-headers参数定义,包括Cookie、Set-Cookie、Authorization三个属性,多个值逗号隔开。在Web开发时Cookie在Spring Cloud Zuul网关中默认不传递,当你使用了Shiro、Spring Security等安全框架构建的Web应用由于Cookie无法传递会导致无法登录和授权。解决的办法有:
设置全局参数为空来覆盖默认值
zuul.sensitive-headers=
这种方法不推荐使用,这样就破坏了默认设置的用意。
指定路由的参数来配置,方法有两种,推荐使用,这种方式会覆盖全局设置。
# 方法一,对指定路由开启自定义敏感头
zuul.routes..customSensitiveHeaders=true
# 方法二,对指定路由的敏感头设置为空
zuul.routes..sensitiveHeaders=
例子:
zuul:
routes:
api-a:
path: /api-a/**
serviceId: hello-service
sensitive-headers:
custom-sensitive-headers: true
忽略Headers
可用 zuul.ignored-headers 属性丢弃一些 Header,这样设置后 Header1、Header2 将不会传播到其它微服务中。
zuul:
ignored-headers: Header1,Header2
在默认情况下是没有这个配置的,如果项目中引入了Spring Security,那么Spring Security会自动加上这个配置,默认值为:Pragma,Cache-Control,X-Frame-Options,X-Content-Type-Options,X-XSS-Protection,Expries此时,如果还需要使用下游微服务的Spring Security的Header时,可以增加下面的设置:
zuul.ignoreSecurityHeaders=false
sensitive-headers和ignored-headers的区别
sensitiveHeaders会过滤客户端附带的headers
例如:sensitiveHeaders: X-ABC,如果客户端在发请求是带了X-ABC,那么X-ABC不会传递给下游服务。
ignoredHeaders会过滤服务之间通信附带的headers
例如:ignoredHeaders: X-ABC,如果客户端在发请求是带了X-ABC,那么X-ABC依然会传递给下游服务。但是如果下游服务再转发就会被过滤。
还有一种情况就是客户端带了X-ABC,在ZUUL的Filter中又addZuulRequestHeader("X-ABC", "new"),
那么客户端的X-ABC将会被覆盖,此时不需要sensitiveHeaders。如果设置了sensitiveHeaders: X-ABC,那么Filter中设置的X-ABC依然不会被过滤。
重定向问题
使用Shiro、Spring Security登录成功后,跳转的页面URL是具体的Web应用实例的地址,而不是通过网关的路由地址。因为使用API网关就是想把网关当作统一入口,从而不暴露所有的内部细节,所以这个问题很严重。导致这个问题的原因是,使用Shiro、Spring Security登录成功后通过重定向的方式跳转到登录后的页面,此时的请求结果状态码为302,请求头信息中的Location指向了具体的服务实例地址,而请求头信息中的Host也指向了具体的服务实例IP地址和端口。所以问题的根本原因在于Spring Cloud Zuul在路由请求时,Host信息设置的不正确。解决办法:
zuul.add-host-header=true
Zuul内部使用Ribbon来调用远程URL。 默认情况下,Spring Cloud在第一次调用时会延迟加载Ribbon客户端。 可以使用以下配置更改Zuul的此行为,这会导致在应用程序启动时急切加载与子功能区相关的应用程序上下文。 以下示例显示如何启用预先加载:
zuul:
ribbon:
eager-load:
enabled: true
许多场景下,一个外部请求,可能需要查询Zuul后端多个微服务。比如说一个电影售票系统,在购票订单页上,需要查询电影微服务,还需要查询用户微服务获得当前用户信息。如果让系统直接请求各个微服务,就算Zuul转发,网络开销,流量耗费,时长都不是很好的。这时候我们就可以使用Zuul聚合微服务请求,即应用系统只发送一个请求给Zuul,由Zuul请求用户微服务和电影微服务,并把数据返给应用系统。
具体来讨论Zuul高可用的两种情景。
Zuul客户端也注册到了Eureka Server上
这种情况下,Zuul的高可用非常简单,只需将多个Zuul节点注册到Eureka Server上,就可实现Zuul的高可用。此时,Zuul的高可用与其他微服务的高可用没什么区别。
当Zuul客户端也注册到Eureka Server上时,只需部署多个Zuul节点即可实现其高可用。Zuul客户端会自动从Eureka Server中查询Zuul Server的列表,并使用Ribbon负载均衡地请求Zuul集群。这种场景一般用于Sidecar。
Zuul客户端未注册到Eureka Server上
现实中,这种场景往往更常见,例如,Zuul客户端是一个手机APP——我们不可能让所有的手机终端都注册到Eureka Server上。这种情况下,我们可借助一个额外的负载均衡器来实现Zuul的高可用,例如Nginx、HAProxy、F5等。
Zuul客户端将请求发送到负载均衡器,负载均衡器将请求转发到其代理的其中一个Zuul节点。这样,就可以实现Zuul的高可用。
节选自《Spring Cloud与Docker微服务架构实战》8.10节