一、前言

在微服务的架构中,服务网关就是一个介于客户端与服务端之间的中间层。在这种情况下,客户端只需要跟服务网关交互,无需调用具体的微服务接口。这样的好处在于,客户端可以降低复杂性;对于需要认证的服务,只需要在服务网关配置即可;同样也方便后期微服务的变更和重构,即微服务接口变更只需在服务网关调整配置即可,无需更改客户端代码。

Zuul是一款由Netflix开发的微服务网关开源软件,可以和其自家开发的Eureka,Ribbon和Hystrix配合使用,Spring Cloud对其进行了封装。

二、介绍

2.1 API 网关使用场景

1>黑白名单: 实现通过 IP 地址控制请求的访问
2>日志:实现访问日志的记录,进而实现日志分析,处理性能指标等
3>协议适配:实现通信协议的校验、适配转换的功能
4>身份认证:对请求进行身份认证
5>计流限流:可以设计限流规则,记录访问流量
6>路由:将请求进行内部(服务)转发

2.2 API 网关的实现

业界常用的 API 网关有很多方式,如:Spring Cloud Zuul、 Nginx、Tyk、Kong。本篇介绍的对象正是 Spring Cloud Zuul。

Zuul 是 Netflix 公司开源的一个 API 网关组件,提供了认证、鉴权、限流、动态路由、监控、弹性、安全、负载均衡、协助单点压测等边缘服务的框架。

Spring Cloud Zuul 是基于 Netflix Zuul 的微服务路由和过滤器的解决方案,也用于实现 API 网关。其中,路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入门的基础。而过滤功能是负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。

Spring Cloud Zuul 和 Eureka 进行整合时,Zuul 将自身注册到 Eureka 服务中,同时从 Eureka 中获取其他微服务信息,以便请求可以准确的通过 Zuul 转发到具体微服务上。

三、实战演练

本次测试案例基于之前发表的文章中介绍的案例进行演示,不清楚的读者请先转移至 《Spring Cloud Hystrix服务容错》 进行浏览。

当前的项目列表如下:

项目服务实例实例id端口描述
common-api公用的 api,如:实体类
eureka-serverEureka-Servereureka-server19001注册中心(Eureka 服务端)
eureka-serverEureka-Servereureka-server29002注册中心(Eureka 服务端)
goods-serverGoods-Servergoods-server18801商品服务(Eureka 客户端)
goods-serverGoods-Servergoods-server28802商品服务(Eureka 客户端)
goods-serverGoods-Servergoods-server38803商品服务(Eureka 客户端)
order-serverOrder-Weborder-web8900订单服务(Eureka 客户端)
user-serverUser-Serveruser-server18788用户服务(Eureka 客户端)
user-serverUser-Serveruser-server28789用户服务(Eureka 客户端)
gateway-serverGateWay-Servergateway-server9100服务网关(Eureka 客户端)

创建一个为名 gateway-server 的 Spring Boot 项目。

3.1 添加依赖

1
2
3
4
5
6
7
8
        <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>

3.2 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server:
port: 9100

spring:
application:
name: GateWay-Server

eureka:
instance:
instance-id: gateway-server
prefer-ip-address: true
client:
register-with-eureka: true # 是否向注册中心注册自己
fetch-registry: true # 是否检索服务
service-url:
#defaultZone: http://127.0.0.1:8761/eureka/
defaultZone: http://eureka01:9001/eureka/,http://eureka02:9002/eureka/

3.3 启动 Zuul

在启动类上添加 @EnableZuulProxy 注解:

1
2
3
4
5
6
7
8
9
@EnableZuulProxy
@SpringBootApplication
public class GateWayServerApplication {

public static void main(String[] args) {
SpringApplication.run(GateWayServerApplication.class, args);
}

}

启动上边的所有项目,使用idea的http工具请求订单下单接口

图中,我们首先不经过网关直接访问 order-server 项目请求地址: http://localhost:8900/order/hystrix

之后再修改成访问 gateway-server 项目的请求地址: http://localhost:9100/order-web/order/hystrix

最终,响应结果都一样。

提示http://localhost:9100/order-web/order/hystrix 中第一个 order-web 表示的是注册在 Eureka 上的订单服务名称。

3.4 zuul 常用配置

传统配置

传统配置就是手动指定服务的转发地址,如在yml中配置:

1
2
3
4
5
zuul:
routes:
userweb:
path: /userweb/**
url: http://localhost:8765

通过上面的配置,所有符合/userweb/**规则的访问都将被路由转发到 http://localhost:8765 地址上,即当我们向服务网关访问 http://localhost:9100/userweb/userFeign/user/1 请求的时候,请求将被转发到 http://localhost:8765/userFeign/user/1 服务上.

基于服务名称配置

1
2
3
4
5
6
zuul:
sensitive-headers:
routes:
wno704:
path: /orderGateWay/**
service-id: order-web

将订单服务的路由名称改成 orderGateWay。

使用 http 请求 http://localhost:9100/orderGateWay/order/hystrix 下单接口,运行结果:

请求成功。

基于服务名称的路由配置还可以进行简化,格式为zuul.routes.=,所以我们可以将orderGateWay的路由配置修改为:

1
2
3
4
zuul:
routes:
order-web:
path: /orderGateWay/**

默认路由配置规则

我们尝试访问 http://localhost:9100/user-web/userFeign/user/1

我们压根没配置这个路由,为什么可以正常响应??其实Zuul配合Eureka后将会成一套默认的配置规则。当我们使用服务名称作为请求的前缀路径时,实际上就会匹配上类似下面的默认路由配置:

1
2
3
4
5
zuul:
routes:
user-web:
path: /user-web/**
serviceId: user-web

如果不想启动这个默认配置,我们可以使用zuul.ignored-services配置来关闭,如在yml中加入如下配置之后,再次访问 http://localhost:9100/user-web/userFeign/user/1 将返回404:

1
2
zuul:
ignored-services: user-web

优先级

假如某个请求路径可以和多个路由配置规则相匹配的话,Zuul根据匹配的先后顺序来决定最终使用哪个路由配置。比如:

1
2
3
4
5
6
7
8
zuul:
routes:
userFeign:
path: /userFeign/**
service-id: User-Web
userFeign1:
path: /userFeign/userFeign/**
service-id: User-Web1

当我们访问 http://localhost:9100/userFeign/userFeign/user/1 的时候,userFeign和userFeign1的路由配置都可以匹配上,但由于userFeign先于userFeign1配置,所以最终生效的是userFeign的配置。

假如将userFeign和userFeign1的配置顺序调换,再次访问 http://localhost:9100/userFeign/userFeign/user/1 时将抛出异常,原因是不存在服务名为User-Web1的服务:

com.netflix.zuul.exception.ZuulException: Forwarding error
....
Caused by: com.netflix.client.ClientException: Load balancer does not have available server for client: User-Web1
...

禁用路由:

1
2
3
zuul:
ignored-patterns:
- /orderGateWay/order/place/** #禁用路由

http://localhost:9100/orderGateWay/order/place 无法被正常路由到订单服务,响应返回 404。

本地跳转

Zuul网关除了支持将服务转发到各个微服务上之外,还支持将服务跳转到网关本身的服务上,比如现在yml中有如下一段配置:

1
2
3
4
5
zuul:
routes:
test2:
path: /test2/**
url: forward:/test

我们在gateway-server入口类中加上该REST服务:

1
2
3
4
5
6
7
@RestController
public class TestController {
@GetMapping("/test/hello")
public String hello() {
return "hello zuul";
}
}

当访问 http://localhost:9100/test2/hello 时,Zuul会从本地/test/hello获取服务。

前缀配置:

1
2
zuul:
prefix: /api

所有请求中的 path 需要添加 api 前缀。如: http://localhost:9100/orderGateWay/order/hystrix 需要改成 http://localhost:9100/api/orderGateWay/order/hystrix

头部过滤:

在使用Zuul网关的时候你可能会遇到Cookie丢失的情况,这是因为默认情况下Zuul会过滤掉HTTP请求头中的一些敏感信息,这些敏感信息通过下面的配置设定:

1
2
zuul:
sensitive-headers: Cookie,Set-Cookie,Authorization # 设置全局敏感头,如果为空,表示接收所有敏感头信息

1
2
3
4
5
6
zuul:
routes:
wno704:
path: /orderGateWay/**
service-id: order-web
sensitive-headers: Cookie,Set-Cookie,Authorization # 针对 /orderGateWay/ 的请求设置敏感头信息

使用Zuul另一个常见问题是重定向的问题,可以通过下面的设置解决:

1
2
zuul:
add-host-header: true

四、核心过滤器

Spring Cloud Zuul为各个生命周期阶段实现了一批过滤器,这些过滤器的优先级和作用如下表所示:

生命周期优先级过滤器功能描述
pre-3ServletDetectionFilter标记处理Servlet的类型
pre-2Servlet30WrapperFilter包装HttpServletRequest请求
pre-1FormBodyWrapperFilter包装请求体
route1DebugFilter标记调试标志
route5PreDecorationFilter处理请求上下文供后续使用
route10RibbonRoutingFilterserviceId请求转发
route100SimpleHostRoutingFilterurl请求转发
route500SendForwardFilterforward请求转发
post0SendErrorFilter处理有错误的请求响应
post1000SendResponseFilter处理正常的请求响应

其中优先级数字越小,优先级越高。

要关闭这些过滤器可以在application.yml中按照格式zuul...disable=true配置即可。比如关闭SendResponseFilter过滤器:

1
2
3
4
5
zuul:
SendResponseFilter:
post:
disable:
true

五、Zuul 自定义过滤器

Zuul 的核心技术就是过滤器,该框架提供了 ZuulFilter 接口让开发者可以自定义过滤规则。

我们以身份检验为例,自定义 ZuulFilter 过滤器实现该功能。

5.1 改造user-server

登录接口:
LoginController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RestController
@RequestMapping("/user")
public class LoginController {
@PostMapping("/login")
public Result login(String username, String password, HttpServletResponse response) {


if ("admin".equals(username) && "admin".equals(password)) {
// 模拟生成 token,实际开发中 token 应存放在数据库或缓存中
String token = "123456";
Cookie cookie = new Cookie("token", token);
cookie.setPath("/");
cookie.setMaxAge(60 * 10);
response.addCookie(cookie);

return Result.success();
}

return Result.fail(401, "账号或密码错误");
}
}

5.2 创建 ZuulFilter 过滤器

在 gateway-server 项目中,新建一个过滤器,需要继承 ZuulFilter 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
public class AuthenticationFilter extends ZuulFilter {

/**
* 过滤器类型
*/
@Override
public String filterType() {
return "pre";
}

/**
* 过滤器执行顺序
*/
@Override
public int filterOrder() {
return 0;
}

/**
* 是否开启过滤
*/
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();

boolean flag = request.getRequestURI().contains("/login");
// 如果是登录请求不进行过滤
if (flag) {
System.out.println("========不执行 zuul 过滤方法=======");
} else {
System.out.println("========执行 zuul 过滤方法=======");
}
return !flag;
}

/**
* 过滤器执行内容
*/
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String token = request.getParameter("token");
// 此处模拟获取数据库或缓存中的 token
String dbToken = "123456";
// 此处简单检验 token
if (token == null || "".equals(token) || !dbToken.equals(token)) {
context.setSendZuulResponse(false);
context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
}

return null;
}
}

其中,filterType 有 4 种类型:
1>pre: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。

2>routing:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。

3>post:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

4>error:在其他阶段发生错误时执行该过滤器。

其过滤顺序如下图:

5.3 测试过滤器

运行所有项目,测试操作步骤如下:

测试效果图如下:
请求用户服务的登录接口( http://localhost:9100/api/user/user/login ),请求不执行 zuul 过滤方法,并且请求响应返回的 cookie 包含 token

请求订单服务的下单接口( http://localhost:9100/api/orderGateWay/order/hystrix ),但不携带 token,请求需要执行 zuul 过滤方法,请求响应 401 权限不足

请求订单服务的下单接口( http://localhost:9100/api/orderGateWay/order/hystrix ),携带之前登录接口返回的 token,请求需要执行 zuul 过滤方法,校验通过后路由到订单服务执行之后的操作