在微服务架构模式下,后端服务的实例数一般是动态的,对于客户端而言很难发现动态改变的服务实例的访问地址信息。因此在基于微服务的项目中为了简化前端的调用逻辑,通常会引入 API Gateway 作为轻量级网关,同时 API Gateway 中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。
Spring Cloud 体系中支持 API Gateway 落地的技术就是 Zuul。Spring Cloud Zuul 路由是微服务架构中不可或缺的一部分,提供动态路由,监控,弹性,安全等的边缘服务。Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器。它的具体作用就是服务转发,接收并转发所有内外部的客户端调用。使用 Zuul 可以作为资源的统一访问入口,同时也可以在网关做一些权限校验等类似的功能。
我们这篇说的 Zuul 是 Zuul 1,实际上 Netflix 已经发布了 Zuul 2,不过 Spring 好像并没有将 Zuul 2 整合到 Spring Cloud 生态中的意思,因为它自己做了一个 Spring Cloud Gateway(估计是因为之前 Zuul 2 一直跳票导致等不及了吧)。关于 Spring Cloud Gateway 这个高性能的网关我们以后再说。
搭建zuul模块
pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.top</groupId>
<artifactId>spring-cloud</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>zuul-client-8008</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>zuul-client-8008</name>
<description>Zuul网关8008</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
添加配置
server:
port: 8008
spring:
application:
name: zuul-client
eureka:
client:
service-url:
defaultZone: http://localhost:8001/eureka/,http://localhost:8002/eureka/
启动类上加上@EnableZuulProxy开启网关
@EnableZuulProxy
@SpringBootApplication
public class ZuulClient8008Application {
public static void main(String[] args) {
SpringApplication.run(ZuulClient8008Application.class, args);
}
}
到了这里zuul的网关就已经搭建完毕,同时可以在注册中心看到我们启动的服务
Zuul在注册到eureka之后,默认具备了服务路由功能和负载均衡功能。zuul会去注册中心拉取服务,这个时候zuul就会创建一个路由规则,每个路由规则都包含两部分,一部分是外部请求的匹配规则,另一部分是路由的服务 ID。针对当前示例的情况,Zuul 会创建下面的路由规则:
- 转发到 eureka-client服务的请求规则为:/eureka-client/**
接着我们通过网关来访问eureka-client的方法,注意现在的路径,可以看到请求最终会被路由到eureka-client服务上,eureka-client服务又有两个实例,又会进行轮训访问。
也可以指定服务的映射路径
zuul:
routes:
eureka-client: #可以任意,一般都会和服务名相同
path: /eureka-client/** #访问路径
serviceId: eureka-client #会映射到这个服务上
#url: http://localhost:8003 #也可以直接映射到某个地址,一般不会这么做
Zuul过滤器功能
我们已经能够实现请求的路由功能,所以我们的微服务应用提供的接口就可以通过统一的 API 网关入口被客户端访问到了。但是,每个客户端用户请求微服务应用提供的接口时,它们的访问权限往往都需要有一定的限制,系统并不会将所有的微服务接口都对它们开放。然而,目前的服务路由并没有限制权限这样的功能,所有请求都会被毫无保留地转发到具体的应用并返回结果。
为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后的系统维护难度,因为同一个系统中的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式会使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。所以,比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。在完成了剥离之后,有不少开发者会直接在微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了鉴权逻辑的分离,并没有在本质上将这部分不属于业余的逻辑拆分出原有的微服务应用,冗余的拦截器或过滤器依然会存在。
对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的加入,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用的接口开发和测试复杂度也得到了相应的降低。
Filter 的生命周期
Filter 的生命周期有 4 个,分别是 “PRE”、“ROUTING”、“POST” 和 “ERROR”,整个生命周期可以用下图来表示
Zuul 大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
- PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR:在其他阶段发生错误时执行该过滤器。
Zuul中默认实现的Filter
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理Servlet的类型 |
pre | -2 | Servlet30WrapperFilter | 包装HttpServletRequest请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
自定义Filter
除了默认的过滤器类型,Zuul 还允许我们创建自定义的过滤器类型。
我们假设有这样一个场景,因为服务网关应对的是外部的所有请求,为了避免产生安全隐患,我们需要对请求做一定的限制,比如请求头中含有 Token 便让请求继续往下走,如果请求头不带 Token 就直接返回并给出提示。
首先自定义一个 Filter,继承 ZuulFilter 抽象类,在 run () 方法中验证请求头中是否含有 Token,具体如下:
public class TokenFilter extends ZuulFilter {
/*
* 过滤器的类型,它决定过滤器在请求的哪个生命周期中执行
* 这里定义为pre,代表会在请求被路由之前执行
* @return
*/
@Override
public String filterType() {
return "pre";
}
/**
*filter执行顺序,通过数字指定,数字越大,优先级越低
* @return
*/
@Override
public int filterOrder() {
return 0;
}
/**
* 判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效
* 实际运用中我们可以利用该方法来指定过滤器的有效范围
* @return
*/
@Override
public boolean shouldFilter() {
return true;
}
/**
* 过滤器的具体逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
String token = request.getHeader("token");
if (token == null || token.isEmpty()) {
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(401);
requestContext.setResponseBody("token is empty");
}
return null;
}
}
还需要在启动类上配置该过滤器
@EnableZuulProxy
@SpringBootApplication
public class ZuulClient8008Application {
public static void main(String[] args) {
SpringApplication.run(ZuulClient8008Application.class, args);
}
@Bean
public TokenFilter tokenFilter(){
return new TokenFilter();
}
}
接下来我们启动程序,我们发现没有传token时候,zuul网关会拦截请求
传token之后,请求正常路由到eureka-client服务
最后说几个zuul的配置参数:
zuul:
prefix: /api ##配置前缀,可以看到那么你访问的服务都必须要加/api,例如/api/eureka-client/
ignored-services: ## 忽略的服务
ignored-patterns: ## 忽略的路径
sensitive-headers: cookie ## 设置不禁用的请求头,cookie默认是禁用的
TokenFilter: ##过滤器名
pre: ##过滤器类型
disable: true ##禁用这个过滤器
参考
Spring Cloud(十一):服务网关 Zuul(过滤器)【Finchley 版】
相关阅读
项目代码
SpringCloud 汇总【Greenwich 版】
SpringCloud(一):Eureka注册中心【Greenwich 版】
SpringCloud(二):Ribbon负载均衡【Greenwich 版】
SpringCloud(三):Feign声明式服务调用【Greenwich 版】
SpringCloud(四):Hystrix熔断器介绍【Greenwich 版】
SpringCloud(五):Hystrix的请求熔断与服务降级【Greenwich 版】
SpringCloud(六):Hystrix的请求合并【Greenwich 版】
SpringCloud(七):Hystrix仪表盘与Turbine集群监控【Greenwich 版】
SpringCloud(八):Zuul网关【Greenwich 版】
SpringCloud(九):Config配置中心【Greenwich 版】
SpringCloud(十):Bus消息总线【Greenwich 版】
SpringCloud(十一):Stream消息驱动 + RabbitMQ【Greenwich 版】
SpringCloud(十二):Sleuth链路跟踪【Greenwich 版】