Spring Cloud Gateway 微服务网关

Spring Cloud Netflix Zuul 1.x 是一个基于阻塞 IO 的 API Gateway 以及 Servlet;直到2018年5月,Zuul 2.x(基于Netty,也是非阻塞的,支持长连接)才发布,但 Spring Cloud 暂时还没有整合计划。Spring Cloud Gateway 比 Zuul 1.x 系列的性能和功能整体要好。

1. Spring Cloud Gateway 是什么

1.1. 简述

Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,限流等。它是基于Nttey的响应式开发模式。

Spring Cloud Gateway官方文档:https://spring.io/projects/spring-cloud-gateway#overview

| 组件 | RPS(request per second) | | —- | —- |

| Spring Cloud Gateway | Requests/sec: 32213.38 |

| Zuul 1.x | Requests/sec: 20800.13 |

上表为Spring Cloud Gateway与Zuul的性能对比,从结果可知,Spring Cloud Gateway的RPS是Zuul的1.6倍

1.2. 优缺点

优点:

  • 性能强劲:是第一代网关Zuul的1.6倍
  • 功能强大:内置了很多实用的功能,例如转发、监控、限流等
  • 设计优雅,容易扩展

缺点:

  • 其实现依赖Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
  • 不能将其部署在Tomcat、Jetty等Servlet容器里,只能打成jar包执行
  • 需要Spring Boot 2.0及以上的版本,才支持

    1.3. 核心概念

    路由(route):路由是网关最基础的部分,表示一个具体的路由信息载体。路由信息由一个ID、一个目的地URI、排序order、一组断言工厂和一组Filter组成。如果断言为真,则说明请求URL和配置的路由匹配。
    06-Spring-Cloud-Gateway - 图1

  • id,路由标识符,区别于其他 Route。

  • uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。
  • order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。
  • predicates(断言):Java8中的断言函数,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自Http Request中的任何信息,比如请求头和参数等。
  • filter(过滤器):一个标准的Spring WebFilter,Spring Cloud Gateway中的Filter分为两种类型,分别是Gateway FilterGlobal Filter。过滤器Filter可以对请求和响应进行处理。

    2. Spring Cloud Gateway 基础入门案例

复用11-springcloud-zuul工程的代码,删除zuul网关工程,创建12-springcloud-gateway

2.1. 创建工程导入依赖

12-springcloud-gateway项目中添加新的模块shop-server-gateway,并导入依赖

  1. <!--
  2. Spring Cloud Gateway 服务网关的核心依赖
  3. 注意:SpringCloud Gateway 内部使用的web框架为 netty + webflux
  4. webflux 与 spring-boot-starter-web 依赖存在冲突
  5. -->
  6. <dependency>
  7. <groupId>org.springframework.cloud</groupId>
  8. <artifactId>spring-cloud-starter-gateway</artifactId>
  9. </dependency>

注意:SpringCloud Gateway 内部使用的web框架为netty + webflux,和SpringMVC不兼容。引入的限流组件是hystrix。redis底层不再使用jedis,而是lettuce

2.2. 配置启动类

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

注:Spring Cloud Gateway 组件不需要配置任何注解即可开启

2.3. 配置路由

创建 application.yml 配置文件,配置gateway的路由

  1. server:
  2. port: 8080 # 项目端口
  3. spring:
  4. application:
  5. name: shop-server-gateway # 服务名称
  6. cloud:
  7. # Spring Cloud Gateway 配置
  8. gateway:
  9. # 配置路由(包含的元素:路由id、路由到微服务的uri,断言【判断条件】)
  10. routes:
  11. # 路由配置都是多个,所以此处是一个数组
  12. - id: shop-service-product # 路由id
  13. uri: http://127.0.0.1:9001 # 路由到微服务的uri
  14. predicates:
  15. # 注意此path属性与zuul的path属性不一样,zuul只会将/**部分拼接到uri后面,而gateway会将全部拼接到uri后面
  16. - Path=/product/** # 断言,此处访问 http://127.0.0.1:8080/product/1 就会路由到 http://127.0.0.1:9001/product/1

配置属性说明:

  • id:路由id,保证唯一即可
  • uri:目标服务地址
  • predicates:路由条件。Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)

上面示例的配置解释:配置了一个idshop-service-product的路由规则,当访问网关请求地址以product开头时,会自动转发到地址:http://127.0.0.1:9001/product/xxx。配置完成启动项目即可在浏览器访问进行测试

3. 路由配置规则

3.1. 路由断言功能

Spring Cloud Gateway 的功能很强大,其内置了很多 Predicates 功能。在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,可以通过Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
06-Spring-Cloud-Gateway - 图2
Spring Cloud Gateway 包括许多内置路由断言工厂,所有这些断言都与 HTTP 请求的不同属性匹配。具体如下:

3.1.1. 路由断言 - After

基于Datetime类型的断言工厂 AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期
After的路由判断规则,用于匹配在指定日期时间之后发生的请求

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: after_route
  6. uri: https://example.org
  7. predicates:
  8. # 路由断言匹配在指定日期时间之后发生的请求
  9. - After=2017-01-20T17:42:47.789-07:00[America/Denver]

3.1.2. 路由断言 - Before

基于Datetime类型的断言工厂 BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期
Before的路由判断规则,用于匹配在指定日期时间之前发生的请求

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: before_route
  6. uri: https://example.org
  7. predicates:
  8. # 路由断言匹配在指定日期时间之前发生的请求
  9. - Before=2017-01-20T17:42:47.789-07:00[America/Denver]

3.1.3. 路由断言 - Between

基于Datetime类型的断言工厂 BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否在指定时间段内
Between的路由判断规则,用于两个指定日期时间之间发生的请求

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: between_route
  6. uri: https://example.org
  7. predicates:
  8. # 路由断言匹配在指定两个日期时间之间发生的请求
  9. - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]

3.1.4. 路由断言 - Cookie

基于Cookie的断言工厂 CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。判断请求cookie是否具有给定名称且值与正则表达式匹配。
Cookie的路由判断规则,用于Cookie匹配,此predicate匹配给定名称(chocolate)和正则表达式(ch.p)

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: cookie_route
  6. uri: https://example.org
  7. predicates:
  8. - Cookie=chocolate, ch.p

3.1.5. 路由断言 - Header

基于Header的断言工厂 HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式。判断请求Header是否具有给定名称且值与正则表达式匹配。
Header的路由判断规则,用于Header匹配,header名称匹配X-Request-Id,且正则表达式匹配\d+

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: header_route
  6. uri: https://example.org
  7. predicates:
  8. - Header=X-Request-Id, \d+

3.1.6. 路由断言 - Host

基于Host的断言工厂 HostRoutePredicateFactory:接收一个参数,主机名模式。判断请求的 Host 是否满足匹配规则。
Host的路由判断规则,用于Host匹配,匹配指定的Host主机列表,**代表可变参数

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: host_route
  6. uri: https://example.org
  7. predicates:
  8. - Host=**.somehost.org,**.anotherhost.org

3.1.7. 路由断言 - Method

基于Method请求方法的断言工厂 MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
Method的路由判断规则,用于请求Method匹配,匹配的是请求的HTTP方法

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: method_route
  6. uri: https://example.org
  7. predicates:
  8. # 如果请求方法是GET或POST,则匹配路由
  9. - Method=GET,POST

3.1.8. 路由断言 - Path

基于 Path 请求路径的断言工厂 PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
Path的路由判断规则,用于请求url匹配,{segment}为可变参数

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: path_route
  6. uri: https://example.org
  7. predicates:
  8. - Path=/red/{segment},/blue/{segment}

3.1.9. 路由断言 - Query

基于 Query 请求参数的断言工厂 QueryRoutePredicateFactory:接收两个参数,请求 param 和正则表达式, 判断请求参数是否具有给定名称且值与正则表达式匹配。
Query的路由判断规则,用于匹配请求参数,将请求的参数param(green)进行匹配,也可以进行regexp正则表达式匹配 (参数包含red,并且red的值匹配green或者greet都可以 )

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: query_route
  6. uri: https://example.org
  7. predicates:
  8. # 请求包含绿色查询参数,则匹配路由
  9. - Query=green
  10. # 请求包含red的请求参数,并且值为gree时,匹配路由;如进行regexp正则表达式匹配,则green或者greet均匹配
  11. #- Query=red, gree.

3.1.10. 路由断言 - RemoteAddr

基于远程地址的断言工厂 RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
RemoteAddr的路由判断规则,用于远程IP地址匹配,将匹配192.168.1.1~192.168.1.254之间的ip地址,其中24为子网掩码位数即255.255.255.0

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: remoteaddr_route
  6. uri: https://example.org
  7. predicates:
  8. # 匹配192.168.1.1~192.168.1.254之间的ip地址
  9. - RemoteAddr=192.168.1.1/24

3.1.11. 路由断言 - Weight

基于路由权重的断言工厂 WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
Weight的路由判断规则,用于请求权重匹配。其中有两个参数groupweight,均为int数值类型,用于计算权重

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: weight_high
  6. uri: https://weighthigh.org
  7. predicates:
  8. - Weight=group1, 8
  9. - id: weight_low
  10. uri: https://weightlow.org
  11. predicates:
  12. - Weight=group1, 2

以上示例配置表示:会将大约80%的流量转发到weighthigh.org,将大约20%的流量转发到weightlow.org。

3.2. 动态路由

和 zuul 网关类似,在 Spring Cloud GateWay 组件也支持动态路由:即自动的从注册中心中获取服务列表并访问

3.2.1. 基于 Eureka 注册中心动态获取路由

3.2.1.1. 添加注册中心依赖(Eureka)

12-springcloud-gateway工程的pom文件中添加注册中心的客户端依赖(此示例以Eureka做为注册中心)

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  4. </dependency>
3.2.1.2. 配置动态路由

修改 application.yml 配置文件,添加eureka注册中心的相关配置,并修改访问映射的URL为服务名称

  1. spring:
  2. cloud:
  3. # Spring Cloud Gateway 配置
  4. gateway:
  5. # 配置路由(包含的元素:路由id、路由到微服务的uri,断言【判断条件】)
  6. routes:
  7. # 路由配置都是多个,所以此处是一个数组
  8. - id: shop-service-product # 路由id
  9. uri: lb://shop-service-product # 方式二:根据微服务名称从注册中心拉取服务的地址与端口,格式: lb://服务名称(服务在注册中心上注册的名称)
  10. predicates:
  11. # 注意此path属性与zuul的path属性不一样,zuul只会将/**部分拼接到uri后面,而gateway会将全部拼接到uri后面
  12. - Path=/product/** # 断言,此处访问 http://127.0.0.1:8080/product/1 就会路由到 http://127.0.0.1:9001/product/1
  13. # Eureka 配置
  14. eureka:
  15. instance:
  16. prefer-ip-address: true # 将当前服务的ip地址注册到Eureka服务中
  17. instance-id: ${spring.application.name}:${server.port} # 指定实例id
  18. client:
  19. service-url:
  20. defaultZone: http://localhost:8001/eureka/ # Eureka server 地址,多个eureka server之间用,隔开

配置动态路由要点:配置uri属性以lb://开头(lb代表从注册中心获取服务),后面接的就是需要转发到的服务名称
测试访问网关请求地址以product开头时,会通过注册中心获取转发的地址,自动转发到地址:http://127.0.0.1:9001/product/xxx。配置完成启动项目即可在浏览器访问进行测试

3.2.2. 基于 Nacos 注册中心动态获取路由

3.2.2.1. 添加注册中心依赖(Nacos)

spring-cloud-alibaba-2.1.x-sample\api-gateway工程的 pom 文件中添加注册中心的客户端依赖(此示例以 Nacos 做为注册中心)

  1. <!-- nacos 客户端 -->
  2. <dependency>
  3. <groupId>com.alibaba.cloud</groupId>
  4. <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
  5. </dependency>
3.2.2.2. 开启 nacos 客户端

在项目启动类或者配置上添加注解 @EnableDiscoveryClient,开启 nacos 客户端

  1. @SpringBootApplication
  2. @EnableDiscoveryClient
  3. public class ApiGatewayApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(ApiGatewayApplication.class, args);
  6. }
  7. }
3.2.2.3. 配置动态路由

修改项目的 application.yml 配置文件,具体修改内容如下:

  1. 添加 nacos 注册中心的相关配置,将网关服务注册到 nacos 中,
  2. 修改访问映射的uri,改为注册中心上相应的服务名称 ``` server: port: 7000 # 项目端口 spring: application: name: api-gateway # 服务名称 cloud:

    Spring Cloud Gateway 配置

    gateway: discovery:
    1. locator:
    2. enabled: true # 配置开启让 gateway 从 nacos 注册中心中获取服务信息列表

    配置路由数组(包含的元素:路由id、路由到微服务的uri,断言【判断条件】)

    routes:
    1. # 路由配置都是多个,所以此处是一个数组
    2. - id: service-product # 路由id
    3. # 方式二:根据微服务名称从注册中心拉取服务的地址与端口,格式: lb://服务名称(服务在注册中心上注册的名称)。
    4. # lb 是 Load Balance 的缩写,gateway 遵循实现了负载均衡策略
    5. uri: lb://service-product
    6. order: 1 # 路由的优先级,数字越小级别越高
    7. predicates: # 断言(就是路由转发要满足的条件)
    8. # 注意此path属性与zuul的path属性不一样,zuul只会将/**部分拼接到uri后面,而gateway会将全部拼接到uri后面
    9. # 断言,此处访问 http://127.0.0.1:7000/api-product/product/1 就会路由到 http://127.0.0.1:8081/api-product/product/1(在未配置RewritePath属性前、StripPrefix 过滤器之前)
    10. - Path=/api-product/**
    11. filters: # 过滤器,请求在传递过程中可以通过过滤器对其进行一定的修改
    12. - StripPrefix=1 # 此过滤器配置的作用是,在请求转发之前去掉第1层路径,即以上请求转化路径会变成 http://127.0.0.1:8081/product/1
    nacos: discovery:
    1. server-addr: 127.0.0.1:8848 # 配置 Nacos server 的地址,将网关服务注册到 nacos 中
  1. 测试访问网关请求地址以`api-product`开头时,会通过注册中心获取转发的地址,自动转发到地址:`http://127.0.0.1:8081/product/xxx`。配置完成启动项目即可在浏览器访问进行测试
  2. #### 3.2.3. 动态路由简写版
  3. 当路由配置时,直接通过服务注册的名称来请求,则可以省略 `routes` 的相关的配置。如下:

server: port: 7000 # 项目端口 spring: application: name: api-gateway # 服务名称 cloud:

  1. # Spring Cloud Gateway 配置
  2. gateway:
  3. discovery:
  4. locator:
  5. enabled: true # 配置开启让 gateway 从 nacos 注册中心中获取服务信息列表
  6. # 通过服务注册的名称请求,则可以省略不配置。即 http://127.0.0.1:7000/service-product/product/xx

routes:

- id: service-product # 路由id

uri: lb://service-product

order: 1

predicates:

- Path=/api-product/**

filters:

- StripPrefix=1

  1. nacos:
  2. discovery:
  3. server-addr: 127.0.0.1:8848 # 配置 Nacos server 的地址,将网关服务注册到 nacos 中
  1. 测试,原来的请求路径已经无效了。<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20220105102635258_11205.png)<br />测试通过服务注册的名称,按照`网关地址/微服务/接口`的格式去访问,路由可以成功转发。即`http://127.0.0.1:7000/service-product/product/xx` 默认相当于转发 `http://127.0.0.1:8081/product/xx`。如果请求服务集群,也是能生效。<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20220105102718532_5629.png)
  2. ### 3.3. 重写转发路径
  3. Spring Cloud Gateway 中,路由转发是直接将匹配的路由(path)直接拼接到映射路径(uri)之后,那么在微服务开发中一般会通过 `RewritePath` 机制来进行路径重写。
  4. #### 3.3.1. 官方文档示例
  5. `RewritePath GatewayFilter factory`采用了路径的正则表达式参数和替换参数。通过正则表达式来提供了一种灵活的方式来重写请求路径。以下`RewritePath GatewayFilter`配置示例:

spring: cloud: gateway: routes:

  1. - id: rewritepath_route
  2. uri: https://example.org
  3. predicates:
  4. - Path=/red/**
  5. filters:
  6. - RewritePath=/red(?<segment>/?.*), $\{segment}
  1. 以上示例是:将请求路径`/red/blue`,在请求相应下游服务前,将请求路径重写成`/blue`。**请注意,由于YAML规范,应将`$`替换为`$\`**
  2. #### 3.3.2. 案例改造
  3. 修改`12-springcloud-gateway`工程`application.yml`配置文件,将匹配路径`Path`改为 `/shop-service-product/**`。重新启动网关服务,在浏览器访问`http://127.0.0.1:8080/shop-service-product/product/1`时会抛出404。这是由于路由转发规则默认转发到商品微服务(`http://127.0.0.1:9001/shop-service-product/product/1`)路径上,而商品微服务又没有`shop-service-product`对应的映射配置。<br />在配置文件中添加`RewritePath`属性重写转发路径规则,通过RewritePath配置重写转发的url,将`/shop-service-product/(?.*)`,重写为`{segment}`,然后转发到订单微服务。比如在网页上请求`http://localhost:8080/shop-service-product/product`,此时会将请求转发到`http://127.0.0.1:9001/product/1`(**需要注意的是在yml格式中`$`要写成`$\`**)

spring: application: name: shop-server-gateway # 服务名称 cloud:

  1. # Spring Cloud Gateway 配置
  2. gateway:
  3. # 配置路由(包含的元素:路由id、路由到微服务的uri,断言【判断条件】)
  4. routes:
  5. # 路由配置都是多个,所以此处是一个数组
  6. - id: shop-service-product # 路由id
  7. uri: lb://shop-service-product # 方式二:根据微服务名称从注册中心拉取服务的地址与端口,格式: lb://服务名称(服务在注册中心上注册的名称)
  8. predicates:
  9. - Path=/shop-service-product/**
  10. filters: # 配置路由过滤器
  11. # 配置路径重写的过滤器,通过正则表达式将 http://127.0.0.1:8080/shop-service-product/product/2 重写为 http://127.0.0.1:9001/product/2(注:在yml格式中,$ 需要写写成 $\)
  12. - RewritePath=/shop-service-product/(?<segment>.*), /$\{segment}
  1. > _注:属性名称对大小写敏感,在做示例的就将`Path`属性写成`path`,结果后台一直报错说无法映射路径_
  2. ### 3.4. 简化路径配置,根据微服务名称转发请求
  3. Spring Cloud Gateway提供了可以直接从注册中心,根据相应的服务名称来进行请求路径转发的简化配置
  4. #### 3.4.1. 传统手动配置路由转发
  5. 在未配置开启从注册中心自动根据服务名称映射请求转发路径前,通过网关访问订单服务,会转发失败。因为没有配置相应服务的路由匹配规则<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030094305528_7738.png)
  6. #### 3.4.2. 配置根据服务名称自动转发

spring: application: name: shop-server-gateway # 服务名称 cloud:

  1. # Spring Cloud Gateway 配置
  2. gateway:
  3. # 配置自动根据注册中心的微服务名称进行路由转发
  4. discovery:
  5. locator:
  6. enabled: true # 开启根据服务名称自动转发,默认值是false
  7. lower-case-service-id: true # 配置微服务名称以小写的形式匹配,默认值是false,全大写
  1. 配置自动根据服务名称进行请求转发,**默认的匹配规则是:请求路径以服务名称开头,会自动匹配到相应服务的ip+端口,并且将服务名称以后的部分拼接到服务的url的后面**。此时访问相应微服务名称的路径时,请求会转发到相应的服务<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030095931692_5394.png)<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030094951563_21178.png)
  2. ### 3.5. 自定义路由断言工厂
  3. Spring Cloud GateWay 内置的断言基本上已经满足大部分的需要,但有些可能还是需要自定义一些路由断言逻辑。<br />案例需求:仅仅让请求参数 `range` `(min,max)` 之间的人来访问。具体实现步骤如下:
  4. - 修改网关项目的 application.yml 配置文件,添加自定义的断言:

server: port: 7000 spring: application: name: api-gateway cloud: gateway: discovery: locator: enabled: true routes:

  1. - id: service-product # 路由id
  2. uri: lb://service-product
  3. predicates: # 断言
  4. - Path=/api-product/**
  5. # 自定义路由断言,具体实现range限制范围只有在18到26岁之间请求能访问
  6. - Custom=18,26
  7. filters:
  8. - StripPrefix=1
  9. nacos:
  10. discovery:
  11. server-addr: 127.0.0.1:8848 # 配置 Nacos server 的地址,将网关服务注册到 nacos 中
  1. - 自定义一个路由断言工厂类,实现断言判断逻辑。断言工厂类实现需要满足以下两个要求:
  2. 1. 类的名称必须是以`RoutePredicateFactory`结尾。即`自定义断言配置名称+RoutePredicateFactory`
  3. 1. 类必须继承 `AbstractRoutePredicateFactory<配置类>` 抽象类
  4. -
  5. > 技巧提示:如不知道如何实现这些接口,可以通过参考框架自身内置的实现来进行改造

// AbstractRoutePredicateFactory 的泛型 C 是一个配置类,配置类用于接收中在配置文件(properties/yml)中的配置值 @Component public class CustomRoutePredicateFactory extends AbstractRoutePredicateFactory {

  1. // 构造函数,调用父类构造器,并传入配置类
  2. public CustomRoutePredicateFactory() {
  3. super(CustomRoutePredicateFactory.Config.class);
  4. }
  5. /*
  6. * 读取配置文件的中参数值,赋值到配置类中的属性上
  7. */
  8. @Override
  9. public List<String> shortcutFieldOrder() {
  10. // 注意,定义返回集合中元素的位置的顺序,必须跟配置文件中的参数值的顺序相对应
  11. return Arrays.asList("min", "max");
  12. }
  13. /**
  14. * 断言核心的处理逻辑
  15. *
  16. * @param config
  17. * @return
  18. */
  19. @Override
  20. public Predicate<ServerWebExchange> apply(CustomRoutePredicateFactory.Config config) {
  21. // Predicate<T> 是 jdk8 的函数式接口,可以直接使用 lambda 表达式
  22. return serverWebExchange -> {
  23. // 获取请求传的参数 range 的值
  24. String range = serverWebExchange.getRequest().getQueryParams().getFirst("range");
  25. // 对范围参数进行判断
  26. if (StringUtils.hasText(range)) {
  27. // 非空,则进行范围的判断
  28. int num = Integer.parseInt(range);
  29. return num >= config.getMin() && num <= config.getMax();
  30. }
  31. return false;
  32. };
  33. }
  34. /**
  35. * 配置类,用于接收在配置文件中的对应参数值
  36. */
  37. public static class Config {
  38. private int min;
  39. private int max;
  40. public Config() {
  41. }
  42. public int getMin() {
  43. return min;
  44. }
  45. public void setMin(int min) {
  46. this.min = min;
  47. }
  48. public int getMax() {
  49. return max;
  50. }
  51. public void setMax(int max) {
  52. this.max = max;
  53. }
  54. }

}

  1. - 测试,请求带上参数`range`,如果超出范围,则请求转发失败

http://127.0.0.1:7000/api-product/product/2 http://127.0.0.1:7000/api-product/product/2?range=19 http://127.0.0.1:7000/api-product/product/2?range=33

  1. ![](https://gitee.com/moonzero/images/raw/master/code-note/20220105113251508_24769.png)
  2. ## 4. 过滤器
  3. Spring Cloud Gateway 除了具备请求路由功能之外,也支持对请求的过滤。通过 Zuul 网关类似,也是通过过滤器的形式来实现的
  4. ### 4.1. 过滤器基础概述
  5. 过滤器就是在请求的传递过程中,对 **转发请求服务之前** **获取响应之后返回结果之前** 做一些处理
  6. #### 4.1.1. 过滤器的生命周期
  7. Spring Cloud Gateway `Filter` 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre post
  8. - `PRE`:这种过滤器在请求被路由之前调用。可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  9. - `POST`:这种过滤器在路由到微服务以后执行。可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  10. ![](https://gitee.com/moonzero/images/raw/master/code-note/20201030101706521_30318.png)
  11. #### 4.1.2. 过滤器类型
  12. Spring Cloud Gateway Filter 从作用范围可分为另外两种 `GatewayFilter` `GlobalFilter`
  13. - `GatewayFilter`(局部过滤器):应用到单个路由或者一个分组的路由上。_如上面基础入门配置重写转发路径的示例,就是使用了`GatewayFilter`,只作用指定的路由配置上_
  14. - `GlobalFilter`(全局过滤器):应用到所有的路由上
  15. ### 4.2. 局部过滤器
  16. #### 4.2.1. 简介
  17. 局部过滤器(`GatewayFilter`),是针对单个路由的过滤器。可以对访问的 URL 过滤,进行切面处理。在 Spring Cloud Gateway 中通过`GatewayFilter`的形式内置了很多不同类型的局部过滤器。<br />_注:一般在配置局部过滤器针对单个路由设置一些过滤规则时,都会使用 Spring Cloud Gateway 内置的过滤器_
  18. #### 4.2.2. Spring Cloud Gateway 内置局部过滤器
  19. |
  20. 过滤器工厂
  21. | 作用
  22. | 参数
  23. |
  24. | --- | --- | --- |
  25. |
  26. AddRequestHeader
  27. | 为原始请求添加Header
  28. | Header的名称及值
  29. |
  30. |
  31. AddRequestParameter
  32. | 为原始请求添加请求参数
  33. | 参数名称及值
  34. |
  35. |
  36. AddResponseHeader
  37. | 为原始响应添加Header
  38. | Header的名称及值
  39. |
  40. |
  41. DedupeResponseHeader
  42. | 剔除响应头中重复的值
  43. | 需要去重的Header名称及去重策略
  44. |
  45. |
  46. Hystrix
  47. | 为路由引入Hystrix的断路器保护
  48. | `HystrixCommand`的名称
  49. |
  50. |
  51. FallbackHeaders
  52. | fallbackUri的请求头中添加具体的异常信息
  53. | Header的名称
  54. |
  55. |
  56. PrefixPath
  57. | 为原始请求路径添加前缀
  58. | 前缀路径
  59. |
  60. |
  61. PreserveHostHeader
  62. | 为请求添加一个`preserveHostHeader=true`的属性,路由过滤器会检查该属性以决定是否要发送原始的Host
  63. |
  64. |
  65. |
  66. RequestRateLimiter
  67. | 用于对请求限流,限流算法为令版桶
  68. | keyResolverrateLimiterstatusCodedenyEmptyKeyemptyKeyStatus
  69. |
  70. |
  71. RedirectTo
  72. | 将原始请求重定向到指定的UR
  73. | http状态码及重定向的url
  74. |
  75. |
  76. RemoveHopByHopHeadersFilter
  77. | 为原始请求删除IETF组织规定的一系列Header
  78. | 默认就会启用,可以通过配置指定仅删除哪些Header
  79. |
  80. |
  81. RemoveRequestHeader
  82. | 为原始请求删除某个Header
  83. | Header名称
  84. |
  85. |
  86. RemoveResponseHeader
  87. | 为原始响应删除某个Header
  88. | Header名称
  89. |
  90. |
  91. RewritePath
  92. | 重写原始的请求路径
  93. | 原始路径正则表达式以及重写后路径的正则表达式
  94. |
  95. |
  96. RewriteResponseHeader
  97. | 重写原始响应中的某个Header
  98. | Header名称,值的正则表达式,重写后的值
  99. |
  100. |
  101. SaveSession
  102. | 在转发请求之前,强制执行`WebSession::save`操作
  103. |
  104. |
  105. |
  106. secureHeaders
  107. | 为原始响应添加一系列起安全作用的响应头
  108. | 无,支持修改这些安全响应头的值
  109. |
  110. |
  111. SetPath
  112. | 修改原始的请求路径
  113. | 修改后的路径
  114. |
  115. |
  116. SetResponseHeader
  117. | 修改原始响应中某个Header的值
  118. | Header名称,修改后的值
  119. |
  120. |
  121. SetStatus
  122. | 修改原始响应的状态码
  123. | HTTP 状态码,可以是数字,也可以是字符串
  124. |
  125. |
  126. StripPrefix
  127. | 用于截断原始请求的路径
  128. | 使用数字表示要截断的路径的数量
  129. |
  130. |
  131. Retry
  132. | 针对不同的响应进行重试
  133. | retriesstatusesmethodsseries
  134. |
  135. |
  136. RequestSize
  137. | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回`413 Payload Too Large`
  138. | 请求包大小,单位为字节,默认值为5M
  139. |
  140. |
  141. ModifyRequestBody
  142. | 在转发请求之前修改原始请求体内容
  143. | 修改后的请求体内容
  144. |
  145. |
  146. ModifyResponseBody
  147. | 修改原始响应体的内容
  148. | 修改后的响应体内容
  149. |
  150. 以上每个过滤器工厂都对应一个实现类,并且这些类的名称必须以 `GatewayFilterFactory` 结尾,这是 Spring Cloud Gateway 的一个约定,例如 `AddRequestHeader` 对应的实现类为 `AddRequestHeaderGatewayFilterFactory`。对于这些过滤器的使用方式可以参考官方文档
  151. > 官方内置过滤器参考(2.2.5.RELEASE版本):[https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories](https://docs.spring.io/spring-cloud-gateway/docs/2.2.5.RELEASE/reference/html/#gatewayfilter-factories)
  152. #### 4.2.3. 自定义局部过滤器
  153. > 自定义局部过滤器与自定义路由断言工厂的步骤一样
  154. 案例需求:给请求增加一个是否开启日志记录的操作。具体步骤如下
  155. - 修改网关项目的 application.yml 配置文件,添加自定义的局部过滤器:

server: port: 7000 spring: application: name: api-gateway cloud: gateway: discovery: locator: enabled: true routes:

  1. - id: service-product # 路由id
  2. uri: lb://service-product
  3. predicates: # 断言
  4. - Path=/api-product/**
  5. filters:
  6. - StripPrefix=1
  7. - Log=true,false # 自定义局部过滤器,具体示例实现是否开启日志记录
  8. nacos:
  9. discovery:
  10. server-addr: 127.0.0.1:8848 # 配置 Nacos server 的地址,将网关服务注册到 nacos 中
  1. - 自定义一个自定义局部过滤器,实现相关功能处理逻辑。自定义局部过滤器实现需要满足以下两个要求:
  2. 1. 类的名称必须是以`GatewayFilterFactory`结尾。即`自定义局部过滤器名称+GatewayFilterFactory`
  3. 1. 类必须继承 `AbstractGatewayFilterFactory<配置类>` 抽象类

// AbstractGatewayFilterFactory 的泛型 C 是一个配置类,配置类用于接收中在配置文件(properties/yml)中的配置值 @Component public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory {

  1. // 构造函数,调用父类构造器,并传入配置类
  2. public LogGatewayFilterFactory() {
  3. super(LogGatewayFilterFactory.Config.class);
  4. }
  5. /**
  6. * 读取配置文件的中参数值,赋值到配置类中的属性上
  7. *
  8. * @return
  9. */
  10. @Override
  11. public List<String> shortcutFieldOrder() {
  12. return Arrays.asList("consoleLog", "cacheLog");
  13. }
  14. /**
  15. * 过滤器的核心处理逻辑
  16. *
  17. * @param config
  18. * @return
  19. */
  20. @Override
  21. public GatewayFilter apply(LogGatewayFilterFactory.Config config) {
  22. return (exchange, chain) -> {
  23. // 对配置的参数判断
  24. if (config.isCacheLog()) {
  25. System.out.println("cacheLog已经开启了....");
  26. }
  27. if (config.isConsoleLog()) {
  28. System.out.println("consoleLog已经开启了....");
  29. }
  30. // 传递过滤器链
  31. return chain.filter(exchange);
  32. };
  33. }
  34. /**
  35. * 配置类,用于接收在配置文件中的对应参数值
  36. */
  37. public static class Config {
  38. private boolean consoleLog;
  39. private boolean cacheLog;
  40. public Config() {
  41. }
  42. public boolean isConsoleLog() {
  43. return consoleLog;
  44. }
  45. public void setConsoleLog(boolean consoleLog) {
  46. this.consoleLog = consoleLog;
  47. }
  48. public boolean isCacheLog() {
  49. return cacheLog;
  50. }
  51. public void setCacheLog(boolean cacheLog) {
  52. this.cacheLog = cacheLog;
  53. }
  54. }

}

  1. - 测试
  2. ![](https://gitee.com/moonzero/images/raw/master/code-note/20220105144335849_22685.png)
  3. ### 4.3. 全局过滤器
  4. #### 4.3.1. 简介
  5. 全局过滤器(`GlobalFilter`)作用于所有路由,Spring Cloud Gateway 定义了`GlobalFilter`接口,可以自定义实现自己的`GlobalFilter`。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能,并且全局过滤器也是使用比较多的过滤器。
  6. #### 4.3.2. Spring Cloud Gateway 内置全局过滤器
  7. Spring Cloud Gateway 内部也是通过一系列的内置全局过滤器对整个路由转发进行处理如下:<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030105155813_18563.png)
  8. #### 4.3.3. 自定义全局过滤器案例 - 统一鉴权
  9. Spring Cloud Gateway 内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要自己编写过滤器来实现的,下面示例通过自定义一个全局过滤器,完成统一的权限校验。
  10. ##### 4.3.3.1. 鉴权逻辑
  11. 实现项目开发中的鉴权逻辑一般如下:
  12. - 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
  13. - 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  14. - 以后每次请求,客户端都携带认证的token
  15. - 服务端对token进行解密,判断是否有效。
  16. ![](https://gitee.com/moonzero/images/raw/master/code-note/20201030110827654_2019.png)<br />如上图所示,对于验证用户是否已经登录鉴权的过程可以在网关层统一检验。检验的标准就是请求中是否携带token凭证以及token的正确性。
  17. ##### 4.3.3.2. 案例实现
  18. `12-springcloud-gateway`工程,定义全局过滤器`AuthorizeFilter`,实现`GlobalFilter``Ordered`接口。主要逻辑是去校验所有请求的请求参数中是否包含“token”,如果不包含请求参数“token”则不转发路由,否则执行正常的逻辑。

package com.moon.gateway.filter;

import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;

/**

  • Spring Cloud Gateway 自定义一个全局过滤器,需要实现 GlobalFilter, Ordered接口 */ @Component // 要自定义全局过滤器生效,需要将全局过滤器注册到spring容器中 public class AuthorizeFilter implements GlobalFilter, Ordered {

    / 日志对象 / private static final Logger LOGGER = LoggerFactory.getLogger(AuthorizeFilter.class);

    /**

    • 此方法是过滤器执行的主要逻辑 *
    • @param exchange ServerWebExchange是当前请求和响应的上下文对象,存放着重要的请求-响应属性、请求实例和响应实例等等。(相当于zuul中的RequestContext)
    • @param chain
    • @return */ @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { LOGGER.info(“自定义全局过滤器AuthorizeFilter开始执行了….”); // 通过ServerWebExchange对象可以获取请求与响应实例 ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse();

      // 获取请求头中access-token字段 String token = request.getHeaders().getFirst(“access-token”);

      // 简单的模拟校验 if (StringUtils.isEmpty(token)) {

      1. // 如果请求头不包含Authorization,则认证失败。记录一下日志
      2. LOGGER.error("请求{}, 登陆认证失败", request.getURI());
      3. // 设置响应状态码
      4. response.setStatusCode(HttpStatus.UNAUTHORIZED);
      5. // 设置请求结束
      6. return response.setComplete();

      }

      // 如果认证通过,需要调用 chain.filter() 方法才继续向下游执行 return chain.filter(exchange); }

      /**

    • 指定过滤器的执行顺序。返回值越小,执行优先级越高 *
    • @return */ @Override public int getOrder() { return 0; } }
  1. 示例相关说明:
  2. - 自定义全局过滤器需要实现`GlobalFilter``Ordered`接口
  3. - `filter(ServerWebExchange exchange, GatewayFilterChain chain)`方法中完成过滤器的业务逻辑处理
  4. - `getOrder()`方法用于指定此过滤器的优先级,返回值越大级别越低
  5. - `ServerWebExchange` 就相当于当前请求和响应的上下文,存放着重要的请求-响应属性、请求实例和响应实例等等。一个请求中的`request``response`都可以通过 `ServerWebExchange` 获取
  6. - 在过滤器的`filter()`方法中,如果要继续向下游执行,需调用`chain.filter()`方法进行放行
  7. ##### 4.3.3.3. 测试
  8. 测试在请求头中没有设置`access-token`,请求被拦截<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030144343558_8781.png)<br />在请求头中设置`access-token`,请求成功<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030144524165_16614.png)
  9. ## 5. 网关限流
  10. ### 5.1. 常见的限流算法
  11. #### 5.1.1. 计数器限流算法
  12. 计数器限流算法是最简单的一种限流实现方式。其本质是通过维护一个单位时间内的计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030151415332_9293.png)
  13. #### 5.1.2. 漏桶算法
  14. 漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030151518627_10542.png)<br />为了更好的控制流量,**漏桶算法需要通过两个变量进行控制:一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)**
  15. #### 5.1.3. 令牌桶算法
  16. 令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。<br />在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。<br />放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201030152045455_1850.png)
  17. ### 5.2. 基于Filter的限流
  18. Spring Cloud Gateway 官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂 `RequestRateLimiterGatewayFilterFactory` 实现。在过滤器工厂中是通过Redislua脚本结合的方式进行流量控制。

@ConfigurationProperties(“spring.cloud.gateway.filter.request-rate-limiter”) public class RequestRateLimiterGatewayFilterFactory extends AbstractGatewayFilterFactory { …. }

  1. #### 5.2.1. 环境准备
  2. 因为Spring Cloud Gateway的令牌桶限流是基于Redislua脚本实现的,所以需要准备redis服务端。_本示例项目使用windows版本的redis_<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201106162444138_8121.png)<br />打开redis-cli客户端,输入`monitor`命令,开启redis的监控功能<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201106162619033_23547.png)
  3. #### 5.2.2. 添加 redis 的 reactive 依赖
  4. `shop-server-gateway`工程的pom文件中引入SpringBoot监控平台的起步依赖和redisreactive依赖,代码如下:
org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-data-redis-reactive
  1. #### 5.2.3. 修改 application.yml 配置文件
  2. `shop-server-gateway`工程的application.yml配置文件中加入限流的配置

spring: application: name: shop-server-gateway # 服务名称 cloud:

  1. # Spring Cloud Gateway 配置
  2. gateway:
  3. # 配置路由(包含的元素:路由id、路由到微服务的uri,断言【判断条件】)
  4. routes:
  5. # 路由配置都是多个,所以此处是一个数组
  6. - id: shop-service-product # 路由id
  7. uri: lb://shop-service-product # 方式二:根据微服务名称从注册中心拉取服务的地址与端口,格式: lb://服务名称(服务在注册中心上注册的名称)
  8. predicates:
  9. # 注意此path属性与zuul的path属性不一样,zuul只会将/**部分拼接到uri后面,而gateway会将全部拼接到uri后面
  10. - Path=/shop-service-product/**
  11. filters: # 配置路由过滤器
  12. - name: RequestRateLimiter # 配置使用限流过滤器,是Spring Cloud Gateway提供的内置过滤器
  13. args:
  14. # 使用SpEL表达式,从spring容器中获取bean名称为keyResolver的对象,此对象就是KeyResolver接口的实例
  15. key-resolver: '#{@keyResolver}'
  16. # 令牌桶每秒填充平均速率,示例配置表示:每秒往令牌桶填充1个令牌
  17. redis-rate-limiter.replenishRate: 1
  18. # 令牌桶的上限(总容量),示例配置表示:令牌桶的总容量为3上令牌
  19. redis-rate-limiter.burstCapacity: 3
  20. # 配置路径重写的过滤器,通过正则表达式将 http://127.0.0.1:8080/shop-service-product/product/2 重写为 http://127.0.0.1:9001/product/2
  21. - RewritePath=/shop-service-product/(?<segment>.*), /$\{segment}
  1. application.yml 中添加了redis的信息,并配置了`RequestRateLimiter`的限流过滤器,以下是配置参数的说明:
  2. - `key-resolver`:用于配置提供用于限流的存储在redis的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据`#{@beanName}` Spring 容器中获取 Bean 对象
  3. - `redis-rate-limiter.replenishRate`:令牌桶每秒填充平均速率
  4. - `redis-rate-limiter.burstCapacity`:令牌桶总容量
  5. #### 5.2.4. 创建 KeyResolver 键解析器对象
  6. 为了达到不同的限流效果和规则,可以通过实现 `KeyResolver` 接口,定义不同请求类型的限流键

package com.moon.gateway.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono;

/**

  • KeyResolver配置类,创建 KeyResolver 接口实例,定义不同请求类型的限流键与规则 / @Configuration public class KeyResolverConfiguration { /*

    • 基于请求路径的限流 *
    • @return */ @Bean(“keyResolver”) public KeyResolver pathKeyResolver() { // 示例:根据请求路径做限流依据(路径的值会作为redis的key) return new KeyResolver() {

      1. @Override
      2. public Mono<String> resolve(ServerWebExchange exchange) {
      3. return Mono.just(exchange.getRequest().getPath().toString());
      4. }

      }; }

      /**

    • 基于请求参数的限流 *
    • @return */ // @Bean(“keyResolver”) public KeyResolver userKeyResolver() { // 示例:根据请求参数中的userId做限流依据(userId的值会作为redis的key) return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst(“userId”)); }

      /**

    • 基于请求ip地址的限流 *
    • @return */ // @Bean(“keyResolver”) public KeyResolver ipKeyResolver() { // 示例:根据请求ip做限流依据(ip的值会作为redis的key) return exchange -> Mono.just(exchange.getRequest().getHeaders().getFirst(“X-Forwarded-For”)); } }
  1. #### 5.2.5. 测试
  2. 使用Jmetter模拟5组线程访问<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201107103337876_2134.png)<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201107102546131_2240.png)<br />因为之前加了自定义过滤器进行权限校验,所以这里要加上请求头信息(_也可以将工程中的过滤注释掉_)<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201107103123860_4115.png)<br />结果如下,当达到令牌桶的总容量3时,其他的请求会返回429错误。<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201107103737641_18492.png)<br />通过reids的MONITOR可以监听redis的执行过程。这时候Redis中会有对应的数据:<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201107104108105_18731.png)<br />大括号中就是限流Key,这边是IP,本地的就是localhost
  3. - `timestamp`:存储的是当前时间的秒数,也就是`System.currentTimeMillis()/1000`或者`Instant.now().getEpochSecond()`
  4. - `tokens`:存储的是当前这秒钟的对应的可用的令牌数量
  5. #### 5.2.6. 总结
  6. Spring Cloud Gateway目前提供的限流还是相对比较简单的,在实际项目中限流策略会有很多种情况,比如:
  7. - 对不同接口的限流
  8. - 被限流后的友好提示
  9. 这些可以通过自定义 RedisRateLimiter 来实现自己的限流策略
  10. ### 5.3. 基于 Sentinel 的限流
  11. Sentinel 支持对 Spring Cloud GatewayZuul 等主流的 API Gateway 进行限流。<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201109140057733_8923.png)<br />从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:
  12. - **route 维度**:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
  13. - **自定义 API 维度**:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
  14. #### 5.3.1. 环境搭建
  15. 复用`12-springcloud-gateway`工程的代码创建`13-springcloud-gateway-sentinel`项目,移除不需要的依赖,导入 Sentinel 的相关依赖
  16. 1. 在父聚合项目中引入 Spring Cloud Alibaba 的依赖版本管理
com.alibaba.cloud spring-cloud-alibaba-dependencies 2.1.2.RELEASE pom import
  1. 2. `shop-server-gateway`网关工程中引入sentinel的限流依赖
com.alibaba.csp sentinel-spring-cloud-gateway-adapter
  1. > 注:也可以不引入`spring-cloud-alibaba-dependencies`的依赖,直接在gateway工程中依赖`sentinel-spring-cloud-gateway-adapter`,指定版本号即可(待测试!)
com.alibaba.csp sentinel-spring-cloud-gateway-adapter 1.7.1
  1. #### 5.3.2. 编写Sentinel的配置类

package com.moon.gateway.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver;

import javax.annotation.PostConstruct; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set;

/**

  • Sentinel限流的配置类 */ @Configuration public class SentinelConfiguration {

    private final List viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    /**

    • 构造方法,用于初始化 List 与 ServerCodecConfigurer *
    • @param viewResolversProvider
    • @param serverCodecConfigurer */ public SentinelConfiguration(ObjectProvider> viewResolversProvider,

      1. ServerCodecConfigurer serverCodecConfigurer) {

      this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; }

      /**

    • 配置限流的异常处理器: SentinelGatewayBlockExceptionHandler */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); }

      /**

    • 配置初始化限流过滤器:GlobalFilter */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); }

      /**

    • 配置初始化的限流参数,用于指定资源的限流规则,需要的配置项如下:
      1. 资源名称 (路由id)
      1. 配置统计时间
      1. 配置限流阈值 */ @PostConstruct public void initGatewayRules() { // 创建限流规则 GatewayFlowRule 实例的set集合(因为可以指定多个规则) Set rules = new HashSet<>(); rules.add(new GatewayFlowRule(“shop-service-product”) // 指定限流的资源名称
        1. .setCount(1) // 设置限流的阈值
        2. .setIntervalSec(1) // 设置统计时间,单位是秒,默认是 1 秒
        ); // 添加限流规则到 GatewayRuleManager 管理器 GatewayRuleManager.loadRules(rules); } }
  1. 配置说明:
  2. - 基于 Sentinel Gateway 限流是通过Sentinel内置提供的`Filter`来完成的,使用时只需配置注入对应的 `SentinelGatewayFilter` 实例以及 `SentinelGatewayBlockExceptionHandler` 实例即可
  3. - `@PostConstruct`注解定义初始化的加载方法,用于指定资源的限流规则。上面的示例的资源的名称为`shop-service-product`,统计时间是1秒内,限流阈值是1。表示每秒只能访问一个请求。
  4. #### 5.3.3. 网关限流配置
  5. 修改 `shop-server-gateway` `application.yml` 配置文件,删除基于 Spring Cloud Gateway Filter 的限流配置,只保留路由断言与路由重写的配置即可。**注:路由ID需要与限流设置的一致**

server: port: 8080 # 项目端口 spring: application: name: shop-server-gateway # 服务名称 cloud:

  1. # Spring Cloud Gateway 配置
  2. gateway:
  3. routes:
  4. - id: shop-service-product # 路由id
  5. uri: lb://shop-service-product # 方式二:根据微服务名称从注册中心拉取服务的地址与端口,格式: lb://服务名称(服务在注册中心上注册的名称)
  6. predicates:
  7. - Path=/shop-service-product/**
  8. filters: # 配置路由过滤器
  9. - RewritePath=/shop-service-product/(?<segment>.*), /$\{segment}
  1. #### 5.3.4. 测试
  2. 在一秒钟内多次访问`http://127.0.0.1:8080/shop-service-product/product/2`,就可以看到限流生效了。<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201111171238326_12649.png)
  3. #### 5.3.5. 自定义异常提示
  4. 当触发限流后页面显示的是`Blocked by Sentinel: FlowException`。为了展示更加友好的限流提示,Sentinel支持自定义异常处理。只需要在`GatewayCallbackManager`的静态方法`setBlockHandler`注册回调中进行定制即可:

public final class GatewayCallbackManager { // ….. public static void setBlockHandler(BlockRequestHandler blockHandler){ AssertUtil.notNull(blockHandler, “blockHandler cannot be null”); GatewayCallbackManager.blockHandler = blockHandler; } // ….. }

  1. 静态方法`setBlockHandler`:是注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为`BlockRequestHandler`。默认实现为 `DefaultBlockRequestHandler` ,当被限流时会返回类似于下面的错误信息:`Blocked by Sentinel: FlowException`。<br />在`shop-server-gateway`工程的`SentinelConfiguration`配置类中,增加初始化后执行的方法,注册自定义异常处理逻辑

/**

  • 自定义限流处理器,用于定制异常处理的逻辑 */ @PostConstruct public void initBlockHandlers() { GatewayCallbackManager.setBlockHandler((serverWebExchange, throwable) -> {
    1. Map<String, Object> map = new HashMap<>();
    2. map.put("code", -1);
    3. map.put("message", "不好意思,限流啦");
    4. // 通过 serverWebExchange 上下文对象,设置相应的响应内容
    5. return ServerResponse.status(HttpStatus.OK)
    6. .contentType(MediaType.APPLICATION_JSON_UTF8)
    7. .body(BodyInserters.fromObject(map));
    }); }
  1. 测试结果<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201112083546595_6059.png)
  2. #### 5.3.6. 参数限流
  3. 以上的配置都是针对整个路由来限流的,也可以通过使用参数限流方式,针对某个路由的某个参数做限流。具体的实现是:在配置限流参数`GatewayFlowRule`时,增加对特定的参数限制规则`setParamItem`即可

@PostConstruct public void initGatewayRules() { // 创建限流规则 GatewayFlowRule 实例的set集合(因为可以指定多个规则) Set rules = new HashSet<>(); rules.add(new GatewayFlowRule(“shop-service-product”) // 指定限流的资源名称 .setCount(1) // 设置限流的阈值 .setIntervalSec(1) // 设置统计时间,单位是秒,默认是 1 秒 .setParamItem(new GatewayParamFlowItem() .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) .setFieldName(“id”)) // 指定参数限流,示例是通过指定PARAM_PARSE_STRATEGY_URL_PARAM表示从url中获取参数,setFieldName指定参数名称 ); // 添加限流规则到 GatewayRuleManager 管理器 GatewayRuleManager.loadRules(rules); }

  1. #### 5.3.7. 自定义API分组
  2. 自定义API分组的限流规则,就是用户定义针对不同的请求实现限流的规则,是一种更细粒度的限流规则定义。_示例实现的限流效果与上面一样_
  3. - 在商品微服务定义以下测试接口
  4. ![](https://gitee.com/moonzero/images/raw/master/code-note/20220105164856178_27240.png)
  5. - 以下代码是 `spring-cloud-alibaba-2.1.x-sample` 项目的示例代码。

/**

  • 配置初始化的限流参数,用于指定资源的限流规则,需要的配置项如下:
    1. 资源名称 (路由id)
    1. 配置统计时间
    1. 配置限流阈值 / @PostConstruct public void initGatewayRules() { // 创建限流规则 GatewayFlowRule 实例的 set 集合(因为可以指定多个规则) Set rules = new HashSet<>(); / 创建以下自定义的API限流分组规则,并注册到限流规则管理器中 */ rules.add(new GatewayFlowRule(“product_group1”).setCount(1).setIntervalSec(1)); rules.add(new GatewayFlowRule(“product_group2”).setCount(1).setIntervalSec(1)); // 添加限流规则到 GatewayRuleManager 管理器 GatewayRuleManager.loadRules(rules); }

/*

  • 自定义API限流分组,
  • 1.定义分组
  • 2.对小组配置限流规则 */ @PostConstruct private void initCustomizedApis() { Set definitions = new HashSet<>(); ApiDefinition api1 = new ApiDefinition(“product_group1”)
    1. .setPredicateItems(new HashSet<ApiPredicateItem>() {{
    2. add(new ApiPathPredicateItem().setPattern("/api-product/product/group1/**"). // 以 /product/group1/ 开头都的所有url
    3. setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
    4. }});
    ApiDefinition api2 = new ApiDefinition(“product_group2”)
    1. .setPredicateItems(new HashSet<ApiPredicateItem>() {{
    2. add(new ApiPathPredicateItem().setPattern("/api-product/product/group2/flowlimit1")); // 完全匹配 /product/group2/flowlimit1 的url
    3. }});
    definitions.add(api1); definitions.add(api2); // 添加到 GatewayApiDefinitionManager 接口定义管理器 GatewayApiDefinitionManager.loadApiDefinitions(definitions); }
  1. - 以下代码 `spring-cloud-greenwich-sample` 项目的示例代码。

@PostConstruct public void initGatewayRules() { // 创建限流规则 GatewayFlowRule 实例的set集合(因为可以指定多个规则) Set rules = new HashSet<>(); // 创建以下自定义的API限流分组规则,并注册到限流规则管理器中 rules.add(new GatewayFlowRule(“product_api”).setCount(1).setIntervalSec(1)); // 添加限流规则到 GatewayRuleManager 管理器 GatewayRuleManager.loadRules(rules); }

/*

  • 自定义API限流分组,
  • 1.定义分组
  • 2.对小组配置限流规则 */ @PostConstruct private void initCustomizedApis() { Set definitions = new HashSet<>(); ApiDefinition api1 = new ApiDefinition(“product_api”)
    1. .setPredicateItems(new HashSet<ApiPredicateItem>() {{
    2. add(new ApiPathPredicateItem().setPattern("/shop-service-product/product/**"). // 以 /shop-service-product/product/ 开头都的所有url
    3. setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
    4. }});
    ApiDefinition api2 = new ApiDefinition(“order_api”)
    1. .setPredicateItems(new HashSet<ApiPredicateItem>() {{
    2. add(new ApiPathPredicateItem().setPattern("/shop-service-order/order")); // 完全匹配 /shop-service-order/order 的url
    3. }});
    definitions.add(api1); definitions.add(api2); // 添加到 GatewayApiDefinitionManager 接口定义管理器 GatewayApiDefinitionManager.loadApiDefinitions(definitions); }
  1. ## 6. 网关高可用
  2. **高可用HA**(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。单点服务设计往往是系统高可用最大的风险点,应该尽量在系统设计的过程中避免单点服务设计。方法论上,高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂掉后整个服务会受影响;如果有冗余备份,挂了还有其他备用节点能够顶上。<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201109171300708_23038.png)<br />实际使用 Spring Cloud Gateway 的方式如上图,同时启动多个 Gateway 实例进行负载,不同的客户端使用不同的负载将请求分发到后端的 Gateway 服务,Gateway 再通过HTTP调用后端服务,最后对外输出。因此为了保证 Gateway 的高可用性,可以请求到达 Gateway 前的使用 Nginx 或者 F5 进行负载转发以达到高可用性。
  3. ### 6.1. 配置多个Gateway工程
  4. 修改`13-springcloud-gateway-sentinel`工程`shop-server-gateway`application.yml配置文件,配置通过参数指定项目的端口号:

server: port: ${PORT:8080} # 项目端口

  1. 通过配置不同的`PORT`参数,启动多个网关服务,请求端口分别为80808081。浏览器验证发现效果是一致的
  2. ### 6.2. 配置nginx
  3. 修改nginx配置文件,`nginx-1.18.0\conf\nginx.conf`,添加以下配置

配置多台服务器(这里只在一台服务器上的不同端口)

upstream gateway { server 127.0.0.1:8081; server 127.0.0.1:8080; }

请求转向gateway 定义的服务器列表

location / { proxy_pass http://gateway; }

`` 在浏览器上通过访问http://127.0.0.1/shop-service-product/product/2`请求的效果和之前是一样的。关闭一台网关服务器,还是可以支持部分请求的访问。

7. Spring Cloud Gateway 执行流程分析

06-Spring-Cloud-Gateway - 图3
Spring Cloud Gateway 核心处理流程如上图所示

  1. Gateway 的客户端向 Spring Cloud Gateway 发送请求,请求首先被 HttpWebHandlerAdapter 进行提取组装成网关上下文,然后网关的上下文会传递到DispatcherHandler
  2. DispatcherHandler 是所有请求的分发处理器,DispatcherHandler主要负责分发请求对应的处理器。比如请求分发到对应的 RoutePredicateHandlerMapping (路由断言处理映射器)。
  3. RoutePredicateHandlerMapping 路由断言处理映射器主要作用用于路由查找,根据路由断言判断路由是否可用,以及找到路由后返回对应的FilterWebHandler
  4. FilterWebHandler 主要负责组装 Filter 链,先调用执行一系列的 PreFilter 处理,然后再把请求转到后端对应的代理服务处理,处理完毕之后再执行一系列的 Post Filter,最后将Response返回到 Gateway 客户端。