跳到主要内容

SpringCloud Gateway

Gateway 是什么?​

API 网关是所有请求的入口,承载了所有的流量,API Gateway 是一个门户一样,也可以说是进入系统的唯一节点。这跟面向对象设计模式中的 Facet模式(外观)很像。(说白了就是微服务太多了,得有个提供统一的接口)

API Gateway 封装内部系统的架构,并且提供API给各个客户端。它还可能有其他功能,如授权、监控、负载均衡、缓存、请求分片和管理、静态响应处理等

下图表示,没有网关的情况,客户端的请求会直接落到后端的各个服务中,无法集中统一管理。

下图表示,有网关的情况,所有的请求都先经过网关,然后进行分发到对应服务

API 网关的重要性​

API 网关在微服务项目中是很重要的,网关提供一个统一的管理,服务间的调度变得有序

例子介绍了一个庞杂的电商系统,按照微服务理论进行设计,有如下各种服务:

  • 购物车服务:购物车中的物品数量
  • 订单服务:订单历史记录
  • 目录服务:基本产品信息,例如其名称,图像和价格
  • 审核服务:客户审核
  • 库存服务:库存不足警告
  • 运送服务:运送选项,期限和费用与运送提供商的API分开提取
  • 推荐服务:建议项目

在不使用网关的情况,客户端直接调用各服务:

理想情况,各服务调用是可以正常使用的,但是随着业务拓展,服务之间的调用越来越复杂,到时候系统就会变成如图:

如果没有一个统一的管理,肯定是不合理的,所以可以引入网关,作为一个统一的门户,如图:

API Gateway 的常见用途​

动态路由​

网关可以做路由转发,假如服务信息变了,只要改网关配置既可,所以说网关有动态路由(Dynamic Routing)的作用,如图:

请求监控​

请求监控可以对整个系统的请求进行监控,详细地记录请求响应日志,如图,可以将日志丢到消息队列,如果没有使用网关的话,记录请求信息需要在各个服务中去做

认证鉴权​

认证鉴权可以对每一个访问请求做认证,拒绝非法请求,保护后端的服务,不需要每个服务都做鉴权,在项目中经常有加上 OAuth2.0、JWT,Spring Security进行权限校验

压力测试​

有网关的系统,如果要要对某个服务进行压力测试,可以如图所示,改下网关配置既可,测试请求路由到测试服务,测试服务会有单独的测试数据库,这样测试的请求就不会影响到正式的服务和数据库

三大核心概念​

Route 路由:路由是构建网关的基本模块,它由ID,目标URI,一系列的断言 Predicates和过滤器 Filters组成,如果断言为true,则匹配该路由。

Predicate 断言:开发人员可以匹配 HTTP请求中的所有内容,例如请求头或请求参数,如果请求与断言相匹配则进行路由。

Filter 过滤:Spring 框架中 GatewayFilter的实例,使用过滤器,可以载请求被路由前或者后对请求进行修改。

Gateway 的工作流程​

1 客户端向Spring Cloud Gateway发出请求,请求会发送到网关,DispatcherHandler 是 Http 请求的中央分发器,将请求匹配到相应的 HandlerMapping

2 请求和处理器之间有一个映射关系,网关将会对请求与路由匹配,即 handler 会通过 RoutePredicateHandlerMapping,以匹配到对应的 Route

3 经过 RoutePredicateHandlerMapping 处理后,请求会发送到 Web处理器,该 WebHandler代理了一系列网关过滤器和全局过滤,此时会对请求或者响应头进行处理

4 最后转发到具体的代理服务

注意:上图过滤器之间用虚线分开是因为过滤器可能会发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑,例如 Filter 在 “pre” 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在 “post” 类型的过滤器中可以做响应内容、响应头的修改,志的输出,流量监控等操作。

简单的配置使用​

首先引入网关的依赖

<!-- 注意,网关无需引入 spring-boot-starter-web 依赖,否则会报错 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

不要在网关引入 spring-boot-starter-web 依赖,因为它内部使用的是 webflux

在 yml 上配置路由

server:
port: 9527

spring:
application:
name: "cloud-gateway"
cloud:
gateway:
routes: # 可以配置多个路由
- id: payment_route # 路由的ID,没有固定规则但要求唯一,简易配合服务名
uri: http://localhost:8001 # 匹配的路由地址
#uri: lb://provider-service # 匹配的路由地址(lb 表示通过名字取得服务)
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由(后面的 ** 通配符可以用来匹配 PathVariable)

- id: payment_route2 # 路由的ID,没有固定规则但要求唯一,简易配合服务名
uri: http://localhost:8001 # 匹配后提供服务的路由地址
#uri: lb://provider-service # 匹配后提供服务的路由地址
predicates:
- Path=/payment/lb/** # 断言,匹配访问这个 Gateway 的路径
#- After=2020-03-15T15:35:07.412+08:00[GMT+08:00]
#- Cookie=username,alsritter
#- Header=X-Request-Id, \d+ # 请求头要有X-Request-Id属性并且值为整数的正则表达式
#- Host=**.alsritter.com
#- Method=GET
#- Query=username, \d+ # 要有参数名username并且值还要啥整数才能路由

启动类

@SpringBootApplication
public class GatewayGateway9527Application {

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

}

使用这个网关去访问,就可以直接访问到映射的目的服务了

上面 yml 配置的就是对这个服务路径的断言,路径相匹配的进行路由

使用硬编码的方式配置网关​

上面使用的是 yml 文件的方式来配置网关,但是 Gateway 也提供使用代码的方式配置

只需编写一个 Bean 就行了,下面配置两个路由示例

@Configuration
public class GateWayConfig {
/**
* 当访问地址 http://localhost:9527/archives 时会自动转发到地址:https://alsritter.icu/archives/
*/
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("payment_route3", // 路由的ID,没有固定规则但要求唯一,简易配合服务名
r -> r.path("/payment/discovery") // 断言,路径相匹配的进行路由
.uri("http://localhost:8001")); // 匹配后提供服务的路由地址

// 再配置一个映射到博客的
routes.route("payment_route4",
r -> r.path("/archives/**")
.uri("https://alsritter.icu/")); // 匹配后提供服务的路由地址

return routes.build();
}
}

配置动态路由​

上面的地址都是写死了,但是实际微服务开发应该使用的是服务名来做路由地址,所以需要在网关这里使用负载均衡

首先将其注册进服务中心

<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:7001/eureka #单机版
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayGateway9527Application {

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

}

然后开启从注册中心动态创建路由的功能,利用微服务名进行路由

spring:
application:
name: "cloud-gateway"
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由

这时在把上面的 uri 地址改一下,注意,协议名不再是 http 而是 li

# uri: http://localhost:8001         # 匹配后提供服务的路由地址
uri: lb://PAYMENT-SERVICE # 匹配后提供服务的路由地址

然后 Gateway 就会自动配置负载均衡了

常用的 Predicate​

全部配置参考 官方文档 Route Predicate Factories 这里转载自 【SpringCloud】Gateway常用的Predicate(十八)

Spring Cloud Gateway 将路由作为 Spring WebFlux HandlerMapping 基础架构的一部分进行匹配。

Spring Cloud Gateway 包括许多内置的路由断言工厂。所有这些断言都与 HTTP 请求的不同属性匹配,将匹配到的 url 路由给指定的微服务。可以将多个路由断言工厂与逻辑 and 语句结合使用。

spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://localhost:8001
predicates:
- Cookie=username, alsritter

如上,这个匹配到的 url,被路由到 http://localhost:8001 这个微服务里面

After Route Predicate​

所述 After 断言有一个参数,一个 datetime(其是Java ZonedDateTime)。该断言匹配在指定日期时间之后发生的请求。下面的示例配置路由后断言:

spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://localhost:80017
predicates:
- After=2020-04-20T23:57:57.308+08:00[Asia/Shanghai]

这路由符合2020年4月20日23:57时区时间(上海)之后的任何请求

Before Route Predicate​

所述 Before 断言有一个参数,一个datetime(其是Java ZonedDateTime)。该断言匹配在指定之前发生的请求 datetime。以下示例配置了路由前断言:

spring:
cloud:
gateway:
routes:
- id: before_route
uri: http://localhost:80017
predicates:
- Before=2020-04-21T23:57:57.308+08:00[Asia/Shanghai]

这路由符合2020年4月21日23:57时区时间(上海)之前的任何请求。

Between Route Predicate​

该 Between 断言有两个参数,datetime1 并且 datetime2 这是 Java ZonedDateTime对象。该断言匹配在之后 datetime1和之前发生的请求 datetime2。该 datetime2参数必须是 datetime1 后面。以下示例配置了路由之间的断言:

spring:
cloud:
gateway:
routes:
- id: between_route
uri: http://localhost:8001
predicates:
- Between=2020-04-20T23:57:57.308+08:00[Asia/Shanghai], 2020-04-21T23:57:57.308+08:00[Asia/Shanghai]

所述 Cookie 断言采用两个参数,该 cookie name和 regexp(其是Java正则表达式)。该断言匹配具有给定名称且其值与正则表达式匹配的cookie。以下示例配置Cookie路由断言:

spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://localhost:8001
predicates:
- Cookie=username, alsritter

测试请求命令:

curl http://localhost:9527/payment/get/1 --cookie "username=alsritter"

Header Route Predicate​

所述Header断言采用两个参数,报头name和一个regexp(其是Java正则表达式)。该断言与具有给定名称且其值与正则表达式匹配的标头匹配。以下示例配置标头路由断言:

spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://localhost:8001
predicates:
- Header=X-Request-Id, \d+

如果请求具有名为X-Request-Id其值与 \d+ 正则表达式匹配的标头(即,其值为一个或多个数字),则此路由匹配。

可以使用测试请求命令:

curl http://localhost:9527/payment/get/1 -H "X-Request-Id:123"

Host Route Predicate​

该 Host 断言需要一个参数:主机名的列表 patterns。该模式是带有 . 分隔符的 Ant样式的模式。断言与 Host匹配模式的标头匹配。以下示例配置主机路由断言:

spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://localhost:8001
predicates:
- Host=**.x.com

如果请求具有这种路由匹配 Host用的头值 **.x.com

测试请求命令:

curl http://localhost:9527/payment/get/1 -H "Host:demo1.x.com"

Method Route Predicate​

所述 Method 断言需要 methods 的参数,它是一个或多个参数:HTTP方法来匹配。以下示例配置方法路由断言:

spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://localhost:8001
predicates:
- Method=GET

测试请求命令:

curl http://localhost:9527/payment/get/1 -X GET

Path Route Predicate​

spring:
cloud:
gateway:
routes:
- id: path_route
uri: http://localhost:8001
predicates:
- Path=/payment/get/**

Query Route Predicate​

所述 Query 断言采用两个参数:所要求的param和可选的regexp(其是Java正则表达式)。以下示例配置查询路由断言:

spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://localhost:8001
predicates:
- Query=green

如果请求包含green查询参数,则前面的路由匹配。

测试请求命令:

curl http://localhost:9527/payment/get/1?green=1

RemoteAddr Route Predicate​

所述 RemoteAddr 断言需要的列表 sources,其是 CIDR的表示法(IPv4或IPv6)的字符串,如 192.168.0.1/16(其中 192.168.0.1 是一个IP地址和16一个子网掩码)。以下示例配置一个 RemoteAddr 路由谓词:

spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://localhost:8001
predicates:
- RemoteAddr=192.168.1.1/24

请求测试命令:

curl http://192.168.1.4:9527/payment/get/1

Weight Route Predicate​

该 Weight断言有两个参数:group和weight(一个int)。权重是按组计算的。以下示例配置权重路由断言:

spring:
gateway:
discovery:
routes:
- id: weight_high
uri: http://localhost:8001
predicates:
- Weight=group1, 8
- id: weight_low
uri: http://localhost:8002
predicates:
- Weight=group1, 2

这条路线会将大约 80%的流量转发到 http://localhost:8001,将大约20%的流量转发到 http://localhost:8002

Filter 过滤器​

转载自 spring cloud gateway之filter篇

由 Gateway 工作流程,可以知道 filter 有着非常重要的作用,在 “pre” 类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在 “post” 类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等。

当我们有很多个服务时,比如下图中的 user-service、goods-service、sales-service 等服务,客户端请求各个服务的Api时,每个服务都需要做相同的事情,比如鉴权、限流、日志输出等。

对于这样重复的工作,可以在微服务的上一层加一个全局的权限控制、限流、日志输出的 Api Gatewat 服务,然后再将请求转发到具体的业务服务层。这个Api Gateway服务就是起到一个服务边界的作用,外接的请求访问系统,必须先通过网关层。

生命周期​

Spring Cloud Gateway 同 zuul类似,有 “pre” 和 “post” 两种方式的 filter。客户端的请求先经过 “pre” 类型的 filter,然后将请求转发到具体的业务服务,比如上图中的 user-service,收到业务服务的响应之后,再经过 “post” 类型的 filter 处理,最后返回响应到客户端。

与 zuul不同的是,filter除了分为 “pre” 和 “post” 两种方式的 filter外,在 Spring Cloud Gateway中,filter从作用范围可分为另外两种,一种是针对于单个路由的 gateway filter,它在配置文件中的写法同 predict 类似;另外一种是针对于所有路由的 global gateway filer。

使用自带的 Filter​

全部配置参考 官方文档 GatewayFilter Factories 参考资料 Spring cloud gateway 详解和配置使用(文章较长)

Route filters 可以通过一些方式修改 HTTP请求的输入和输出,针对某些特殊的场景,Spring Cloud Gateway已经内置了很多不同功能的 GatewayFilter Factories。

这里就只记录常用的几个,具体的看文档

AddRequestHeader​

AddRequestHeader GatewayFilter Factory 通过配置name和value可以增加请求的header。

spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-Foo, Bar

对匹配的请求,会额外添加 X-Request-Foo:Bar 的 header。

AddRequestParameter​

AddRequestParameter GatewayFilter Factory 通过配置name和value可以增加请求的参数。

spring:  
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: http://www.google.com
filters:
- AddRequestParameter=foo, bar

对匹配的请求,会额外添加 foo=bar 的请求参数。

AddResponseHeader​

AddResponseHeader GatewayFilter Factory 通过配置name和value可以增加响应的header

spring:  
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://www.google.com
filters:
- AddResponseHeader=X-Response-Foo, Bar

对匹配的请求,响应返回时会额外添加 X-Response-Foo:Bar 的header返回。

自定义配置一个全局 Filter​

/**
* 这个 Ordered 用来控制访问顺序
**/
@Component
@Slf4j
public class MyLogGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("come in MyLogGateWayFilter: " + new Date());

String uname = exchange.getRequest().getQueryParams().getFirst("uname");
//每次进来后判断带不带 uname 这个 key
if (uname == null) {
log.info("用户名为null ,非法用户");
//uname为null非法用户
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}

@Override
public int getOrder() {
return 0;
}
}

注意过滤器执行流程如下,order 越大,优先级越低

Reference​

参考资料 What is API gateway really all about? Java Brains - Brain Bytes 参考资料 SpringCloud系列之API网关(Gateway)服务Zuul 参考资料 Spring Cloud Gateway学习 参考资料 【SpringCloud】Gateway常用的Predicate(十八)