API 网关

API 网关出现的原因是微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

  1. 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  2. 存在跨域请求,在一定场景下处理相对复杂。
  3. 认证复杂,每个服务都需要独立认证。
  4. 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施。
  5. 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难。

以上这些问题可以借助 API 网关解决。API 网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 API 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 API 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:

使用 API 网关后的优点如下:

  • 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
  • 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
  • 减少了客户端与各个微服务之间的交互次数。

Spring Cloud Gateway 简介

Spring Cloud Gateway 是 Spring Cloud 团队的一个全新项目,基于 Spring 5.0、SpringBoot2.0、Project Reactor 等技术开发的网关。 旨在为微服务架构提供一种简单有效统一的 API 路由管理方式。

Spring Cloud Gateway 作为 SpringCloud 生态系统中的网关,目标是替代 Netflix Zuul。Gateway 不仅提供统一路由方式,并且基于 Filter 链的方式提供网关的基本功能。例如:安全,监控/指标,和限流。

Spring Cloud Gateway 本身也是一个微服务,需要注册到 Eureka

网关的核心功能:过滤、路由

核心概念:

  • 路由(route):
  • 断言 Predicate 函数:路由转发规则
  • 过滤器(Filter):

快速入门

1、创建工程

创建一个子工程 gateway-service

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<!--网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Eureka客户端 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

创建启动类com.atguigu.GatewayApplication

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}

2、基础配置

application.yml 配置

1
2
3
4
5
6
7
8
9
10
11
12
# 注释版本
server:
port: 18084
spring:
application:
name: api-gateway # 应用名
# Eureka服务中心配置
eureka:
client:
service-url:
# 注册Eureka Server集群
defaultZone: http://127.0.0.1:7001/eureka

3、路由配置

通过网关配置一个路由功能,用户访问网关的时候,如果用户请求的路径是以/user开始,则路由到user-provider服务去

修改 application.yml 配置即可实现,配置如下:

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
# 注释版本
server:
port: 18084
spring:
application:
name: api-gateway # 应用名
cloud:
gateway:
routes:
# 用户所有以/user开始的请求,都给http://localhost:18081服务处理
# id唯一标识,可自定义
- id: user-service-route
# 路由的服务地址
uri: http://localhost:18081
# 路由拦截的地址配置(断言)
# /user/**所有以/user开始的请求都将被路由到uri指定的服务地址,
# 将该请求交给uri指定的服务处理,比如请求:http://localhost:18084/user/find/2会把请求交给http://localhost:18081/user/find/2处理
predicates:
- Path=/user/**
# Eureka服务中心配置
eureka:
client:
service-url:
# 注册Eureka Server集群
defaultZone: http://127.0.0.1:7001/eureka

概念解释:

  • Route(路由):路由是网关的基本单元,由 ID、URI、一组 Predicate、一组 Filter 组成,根据 Predicate 进行匹配转发。
  • Predicate(谓语、断言):路由转发的判断条件
  • Filter(过滤器):过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容。

4、测试

启动 GatewayApplication 测试

浏览器访问:http://localhost:18084/user/find/2

会访问user-provider服务,效果如下:

5、动态路由

刚才路由规则中,我们把路由的服务地址写死了。

如果服务提供者集群的话,这样做不合理。应该是根据服务名称,去 Eureka 注册中心查找服务对应的所有实例列表,然后进行动态路由。

修改 application.yml,修改映射配置:通过服务名称获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
application:
# 应用名
name: api-gateway
cloud:
gateway:
routes:
# id唯一标识,可自定义
- id: user-service-route
# lb协议表示从Eureka注册中心获取服务请求地址
# user-provider访问的服务名称。
# 路由地址如果通过lb协议加服务名称时,会自动使用负载均衡访问对应服务
uri: lb://user-provider
# 路由拦截的地址配置(断言)
predicates:
- Path=/user/**

路由配置中 uri 所用的协议为 lb 时,gateway 将把 user-provider 解析为实际的主机和端口,并通过 Ribbon 进行负载均衡。

浏览器访问:http://localhost:18084/user/find/2

6、过滤器

过滤器作为 Gateway 的重要功能。常用于请求鉴权、服务调用时长统计、修改请求或响应 header、限流、去除路径等。

① 过滤器的分类

  • 默认过滤器:出厂自带,实现好了拿来就用,不需要实现
    • 全局默认过滤器
    • 局部默认过滤器
  • 自定义过滤器:根据需求自己实现,实现后需配置,然后才能使用
    • 全局过滤器:作用在所有路由上
    • 局部过滤器:配置在具体路由下,只作用在当前路由上

默认过滤器几个,常见如下:

过滤器名称说明
AddRequestHeader对匹配上的请求加上 Header
AddRequestParameters对匹配上的请求路由
AddResponseHeader对从网关返回的响应添加 Header
StripPrefix对匹配上的请求路径去除前缀

详细说明:官方文档

② 默认过滤器配置

默认过滤器有两个:全局默认过滤器和局部默认过滤器。

[1]全局过滤器

对输出响应头设置属性

对输出的响应设置其头部属性名称为 X-Response-Default-MyName,值为 atguigu

修改 gateway-serviceyml 配置文件

1
2
3
4
5
6
7
spring:
cloud:
gateway:
# 配置全局默认过滤器
default-filters:
# 往响应过滤器中加入信息
- AddResponseHeader=X-Response-Default-MyName,atguigu

浏览器访问:http://localhost:18084/user/find/2

查看浏览器响应头信息

[2]局部过滤器配置

通过局部默认过滤器,修改请求路径。局部过滤器在这里介绍两种:添加路径前缀、去除路径前缀。

(1)添加路径前缀

在 gateway 中可以通过配置路由的过滤器 PrefixPath 实现映射路径中的前缀

配置请求地址添加路径前缀过滤器

路由地址信息:

配置访问地址路由地址
PrefixPath=/userhttp://localhost:18084/find/2http://localhost:18081/user/find/2

浏览器请求:http://localhost:18084/find/2时路由器会自动添加/user

全局和局部的区别:全局是对所有的 id 有效,局部只是对某一个 id 有效

(2)去除路径前缀

在 gateway 中通过配置路由过滤器 StripPrefix,实现映射路径中地址的去除。通过 StripPrefix=1 来指定路由要去掉的前缀个数。

如:路径/api/user/1 将会被路由到/user/1。

http://localhost:18084/api/user/2 可以去掉指定个数的前缀 http://localhost:18084/user/2

配置去除路径前缀过滤器

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
spring:
application:
# 应用名
name: api-gateway
cloud:
gateway:
routes:
#id唯一标识,可自定义
- id: user-service-route
#路由的服务地址
#uri: http://localhost:18081
#lb协议表示从Eureka注册中心获取服务请求地址
#user-provider访问的服务名称。
#路由地址如果通过lb协议加服务名称时,会自动使用负载均衡访问对应服务
uri: lb://user-provider
# 路由拦截的地址配置(断言)
predicates:
- Path=/**
filters:
# 请求地址添加路径前缀过滤器
#- PrefixPath=/user
# 去除路径前缀过滤器 添加前缀和去掉前缀不要一起使用
- StripPrefix=1
default-filters:
- AddResponseHeader=X-Response-Default-MyName,atguigu

浏览器访问:http://localhost:18084/api/user/2

路由地址信息:

配置访问地址路由地址
StripPrefix=1http://localhost:18084/api/user/find/2http://localhost:18081/user/find/2
StripPrefix=2http://localhost:18084/api/hello/user/find/2http://localhost:18081/user/find/2

③ 自定义过滤器配置

[1]全局自定义过滤器

自定义全局过滤器的案例:模拟登陆校验。

基本逻辑:如果请求中有 Token 参数,则认为请求有效放行,如果没有则拦截提示授权无效

gateway-service中创建com.atguigu.filter.LoginGlobalFilter全局过滤器类

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
@Component
public class LoginGlobalFilter implements GlobalFilter, Ordered {
/**
* 过滤拦截
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// request获取
ServerHttpRequest request = exchange.getRequest();
// response获取
ServerHttpResponse response = exchange.getResponse();
// 获取请求参数
String token = request.getQueryParams().getFirst("token");
// 判断token是否为null,如果用户请求参数中没有token,则表示用户未登录
if (StringUtils.isEmpty(token)) {
// 没有登录,状态设置401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 结束请求
return response.setComplete();
}
// 如果用户传递了一个token,则表示用户已经登录 ,直接放行
return chain.filter(exchange);
}

/***
* 定义过滤器执行顺序
* 返回值越小,越靠前执行
* @return
*/
@Override
public int getOrder() {
return 0;
}
}

测试:

不携带 token:http://localhost:18084/hahah/user/find/2

携带 token:http://localhost:18084/hahah/user/find/2?token=1

[2]局部过滤器定义

自定义局部过滤器,该过滤器在控制台输出配置文件中指定名称的请求参数及参数的值

局部过滤器作用范围:该局部过滤器在哪个 id 下配置,则该局部过滤器只针对该 id 的路由规则有效

比如这里就是只针对iduser-service-route的路由规则有效。

约定过滤器的前缀为配置的 name,而后面都是GatewayFilterFactory

命名需要以 GatewayFilterFactory 结尾,比如 MyParamGatewayFilterFactory ,那么在使用的时候 MyParam 就是这个过滤器工厂的名称

gateway_service中编写MyParamGatewayFilterFactory

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
@Component
public class MyParamGatewayFilterFactory extends AbstractGatewayFilterFactory {

/**
* 拦截当前id对应的请求
*
* @param config
* @return
*/
@Override
public GatewayFilter apply(Object config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("局部拦截器");
return chain.filter(exchange);
}
};
}

/***
* 构造函数
*/
public MyParamGatewayFilterFactory() {
super(MyParamGatewayFilterFactory.Config.class);
}

/****
* 在该类执行初始化后,可以在这里执行相关初始化操作
*/
public static class Config {
}
}

修改 application.yml 配置文件

测试访问,访问:http://localhost:18084/hahah/user/find/2?token=123