Spring Cloud Gateway

一、为什么需要网关

在企业微服务架构中,一个项目是由多个微服务组成,这些服务有时候会部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如通过浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。这种客户端直接请求服务的方式存在以下问题:

  • 当服务数量众多时,客户端需要维护大量的服务地址,这对于客户端来说,是非常繁琐复杂的。
  • 在某些场景下可能会存在跨域请求的问题。
  • 身份认证的难度大,每个微服务需要独立认证。

上面的问题可以通过网关来解决这些问题

二、什么是网关

API 网关是一个介于客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。API 网关就像整个微服务系统的门面一样,是系统对外的唯一入口。有了它,客户端会先将请求发送到 API 网关,然后由 API 网关根据请求的标识信息将请求转发到微服务实例。

 

对于微服务数量众多、复杂度较高、规模比较大的系统来说,使用  网关具有以下好处:

  • 客户端通过 API 网关与微服务交互时,客户端只需要知道 API 网关地址即可,而不需要维护大量的服务地址,简化了客户端的开发。
  • 客户端直接与 API 网关通信,能够减少客户端与各个服务的交互次数。
  • 客户端与后端的服务耦合度降低。
  • 节省流量,提高性能,提升用户体验。
  • API 网关还提供了安全、流控、过滤、缓存、计费以及监控等 API 管理功能。

三、Spring Cloud Gateway

Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且还基于 Filter 链的方式提供了网关基本的功能。 Spring Cloud 1.0版本 中引用是 Zuul 1.x 版本,而这个版本是基于过滤器的,是阻塞 IO,不支持长连接。而Zuul 2.x 版本一直跳票,2019 年 5 月,Netflix 终于开源了支持异步调用模式的 Zuul 2.0 版本,真可谓千呼万唤始出来。但是 Spring Cloud 官方等不及,已经开始研发了Spring Cloud Gateway,后续已经不再集成 Zuul 2.x 了。

Spring Cloud Gateway 是基于 Spring 生态系统之上构建的 API 网关,包括:Spring 5,Spring Boot 2 和 Project Reactor。Spring Cloud Gateway 旨在提供一种简单而有效的方法来路由到 API,并为它们提供跨领域的关注点,例如:安全性,监视/指标,限流等。Spring Cloud Gateway 是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。

3.1.Spring Cloud Gateway 核心内容

Spring Cloud GateWay 最主要的功能就是路由转发,而在定义转发规则时主要涉及了以下三个核心概念,如下表。

分类描述
Route(路由) 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由
Predicate(断言) 参考的Java8的java.util.function.Predicate开发,路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。
Filter(过滤器) 过滤器,我们可以使用它对请求进行拦截和修改,还可以使用它对上文的响应进行再处理。

需要注意:其中 Route 和 Predicate 必须同时声明。

3.2.Gateway工作流程

Spring Cloud Gateway 工作流程如下图所示:

Spring Cloud Gateway 工作流程说明如下:

  1. 客户端将请求发送到 Spring Cloud Gateway 上。
  2. Spring Cloud Gateway 通过 Gateway Handler Mapping 找到与请求相匹配的路由,将其发送给 Gateway Web Handler。
  3. Gateway Web Handler 通过指定的过滤器链(Filter Chain),将请求转发到实际的服务节点中,执行业务逻辑返回响应结果。
  4. 过滤器之间用虚线分开是因为过滤器可能会在转发请求之前(pre)或之后(post)执行业务逻辑。
  5. 过滤器(Filter)可以在请求被转发到服务端前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等。
  6. 过滤器可以在响应返回客户端之前,对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。
  7. 响应原路返回给客户端。

客户端发送到 Spring Cloud Gateway 的请求需要通过一定的匹配条件,才能定位到真正的服务节点。在将请求转发到服务进行处理的过程前后(pre 和 post),我们还可以对请求和响应进行一些精细化控制。
Predicate 就是路由的匹配条件,而 Filter 就是对请求和响应进行精细化控制的工具。有了这两个元素,再加上目标 URI,就可以实现具体的路由。

3.3.Predicate 断言

Spring Cloud Gateway 通过 Predicate 断言来实现 Route 路由的匹配规则。简单点说,Predicate 是路由转发的判断条件,请求只有满足了 Predicate 的条件,才会被转发到指定的服务上进行处理。
使用 Predicate 断言需要注意以下 3 点:

  • Route 路由与 Predicate 断言的对应关系为“一对多”,一个路由可以包含多个不同断言。
  • 一个请求想要转发到指定的路由上,就必须同时匹配路由上的所有断言。
  • 当一个请求同时满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发。

常见的 Predicate 断言如下表,例如转发的 URI 为 http://localhost:8001。

断言示例说明
Path - Path=/user/list/**  当请求路径与 /user/list/** 匹配时,该请求才能被转发到 http://localhost:8001 上。
Before - Before=2022-10-20T12:50:34.255+08:00[Asia/Shanghai] 在 2022 年 10 月 20 日 12 时 50 分 34.255 秒之前的请求,才会被转发到 http://localhost:8001 上。
After - After=2022-10-20T12:50:34.255+08:00[Asia/Shanghai] 在 2022 年 10 月 20 日 12 时 50 分 34.255 秒之后的请求,才会被转发到 http://localhost:8001 上。
Between - Between=2022-10-22T18:18:33.226+08:00[Asia/Shanghai],2022-10-23T15:23:33.226+08:00[Asia/Shanghai] 在 2022 年 10 月 22 日 18 时 18 分 33.226 秒 到 2022 年 10 月 23 日 15 时 23 分 33.226 秒之间的请求,才会被转发到 http://localhost:8001 服务器上。
Cookie - Cookie=name,augus 携带 Cookie 且 Cookie 的内容为 name=augus 的请求,才会被转发到 http://localhost:8001 上。
Header - Header=X-Request-Id,\d+ 请求头上携带属性 X-Request-Id 且属性值为整数的请求,才会被转发到 http://localhost:8001 上。
Method - Method=GET 只有 GET 请求才会被转发到 http://localhost:8001 上。

四、基础配置

4.1.创建模块 cloud-gateway-gateway9527 作为网关服务

创建maven模块:

设置模块名:

4.2.在pom.xml中引入依赖

依赖中需要包含spring-cloud-starter-gateway

<dependencies>
        <!--引入gateway-->
        <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>

        <!--使用自己定义的api通用包-->
        <dependency>
            <groupId>com.augus.springcloud</groupId>
            <artifactId>cloud-api-common</artifactId>
            <version>${project.version}</version>
        </dependency>
        
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        
    </dependencies>

4.3.创建配置文件

在resources目录下创建配置文件application.yml,内容如下:

server:
  port: 9527

spring:
  application:
    name: cloud-gateway

4.4.创建主启动类

在com.augus.cloud下创建主启动类: GateWayMain9527 ,内容如下:

package com.augus.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

@SpringBootApplication
@EnableEurekaClient //指定注册进入eureka注册中心
public class GateWayMain9527 {
    public static void main(String[] args) {
        SpringApplication.run(GateWayMain9527.class,args);
    }
}

4.5.配置

cloud-provider-payment8001 中controller的访问地址 http://localhost:8001/payment/get/1(查询接口),http://localhost:8001/payment/create?serial=1213(新增接口),我们不想暴露8001端口,希望在8001外面套一层9527网关,修改yml配置文件内容为:

这里路由的配置其实是有两个方式,除了下面显示的在配置文件中配置外,也可以使用api接口配置,但是那种方式不推荐,原因是比较难写。

server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
      routes:
        - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
          #匹配后提供服务的路由地址
          uri: http://localhost:8001
          predicates:
            - Path=/payment/get/** # 断言,路径相匹配的进行路由 **表示url后面省略内容
            - Method=GET # 是有是get请求的时候,才能访问
        - id: payment_route2
          uri: http://localhost:8001
          predicates:
            - Path=/payment/create #断言,路径相匹配的进行路由
            - Method=POST # 只有是post请求的时候才能访问
      enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名称j进行路由


eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

4.6.测试

启动7001,cloud-provider-payment8001,然后再启动cloud-gateway-gateway9527,

设置网关前访问原来的8001的查询和新增的接口分别为:

  • 查询的接口:

  • 新增记录:

 

添加设置网关后,访问:

  • 查询接口:通过网关访问

  • 新增接口:通过网关访问

 

五、通过服务名实现动态

默认情况下Gatway会根据注册中心注册的服务列表上微服务名为路径创建动态路由进行转发,从而实现动态路由的功能

5.1.修改服务提供者8001和8002

给8001和8002的PaymentController中分别添加如下方法:

主要是为了后面测试的使用使用

//进行测试gateway负载均衡
    @GetMapping("/payment/get/service/port")
    public String getPort(){
        return "当前服务者端口是:"+serverPort;
    }

5.2.启动服务

这里启动一个eureka7001、7002和两个服务提供者8001、8002

5.3.修改9527的Gateway的配置文件

修改application.yml内容如下:需要uri的协议lb,表示启用Gateway的负载均衡功能.lb://serverName是spring cloud  gatway在微服务中自动为我们创建的负载均衡uri,下面设置的通过服务的名字通过负载均衡去分别访问8001和8002

server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  #开启从注册中心动态生成路由的功能,用微服务名进行路由
      routes:
        - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/get/** # 断言,路径相匹配的进行路由 **表示url后面省略内容
            - Method=GET # 是有是get请求的时候,才能访问
        - id: payment_route2
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/create #断言,路径相匹配的进行路由
            - Method=POST # 只有是post请求的时候才能访问
        - id: payment_route3 # 路由的id,没有规定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/get/service/port # 断言,路径相匹配的进行路由 **表示url后面省略内容
            - Method=GET # 是有是get请求的时候,才能访问
eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

5.4.测试

启动9527服务,然后访问:http://localhost:9527/payment/get/service/port 会发现8001和8002交替出现:

 

六、 Predicate案例演示

Spring Cloud Gateway 通过 Predicate 断言来实现 Route 路由的匹配规则。下面通过一个案例来进行演示:

6.1.修改9527的服务的配置项

给http://localhost:9527/payment/get/1 添加一个after断言,加上这一句即可:

- After=2025-05-09T09:11:27.551+08:00[Asia/Shanghai] #3.在某个时间点之前--》》可以匹配访问ZonedDateTime格式
server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true  #开启从注册中心动态生成路由的功能,用微服务名进行路由
      routes:
        - id: payment_route # 路由的id,没有规定规则但要求唯一,建议配合服务名
          #匹配后提供服务的路由地址
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/get/** # 断言,路径相匹配的进行路由 **表示url后面省略内容
            - Method=GET # 是有是get请求的时候,才能访问
            - After=2025-05-09T09:11:27.551+08:00[Asia/Shanghai] #3.在某个时间点之前--》》可以匹配访问ZonedDateTime格式
        - id: payment_route2
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/create #断言,路径相匹配的进行路由
            - Method=POST # 只有是post请求的时候才能访问
        - id: payment_route3 # 路由的id,没有规定规则但要求唯一,建议配合服务名
          uri: lb://cloud-payment-service
          predicates:
            - Path=/payment/get/service/port/** # 断言,路径相匹配的进行路由 **表示url后面省略内容
            - Method=GET # 是有是get请求的时候,才能访问

eureka:
  instance:
    hostname: cloud-gateway-service
  client:
    fetch-registry: true
    register-with-eureka: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka

上述的时间可以使用如下代码产生:

import java.time.ZonedDateTime;

public class Test1 {
    public static void main(String[] args) {
        ZonedDateTime now = ZonedDateTime.now();
        System.out.println(now);
    }
}

6.2.测试

上述设置的 2025-05-09T09:11:27.551+08:00[Asia/Shanghai] 时间之后才可以访问,没到这个时间不允许访问的,通过浏览器访问:http://localhost:9527/payment/get/1,会显示如下内容:

七、Filter的使用

项目开发的时候出于安全方面的考虑,服务端提供的服务往往都会有一定的校验逻辑,例如用户登陆状态校验、签名校验等。在微服务架构中,系统由多个微服务组成,这些服务都需要这些校验逻辑,此时我们就可以将这些校验逻辑写到 Spring Cloud Gateway 的 Filter 过滤器中。

7.1.Filter 的分类

Spring Cloud Gateway 提供了以下两种类型的过滤器,可以对请求和响应进行精细化控制。

过滤器类型说明
Pre 类型 这种过滤器在请求被转发到微服务之前可以对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。
Post 类型 这种过滤器在微服务对请求做出响应后可以对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。

按照作用范围划分,Filter 可以分为 2 类:

  • GatewayFilter:应用在单个路由或者一组路由上的过滤器。
  • GlobalFilter:应用在所有的路由上的过滤器。

7.2.GatewayFilter 网关过滤器

GatewayFilter 是 Spring Cloud Gateway 网关中提供的一种应用在单个或一组路由上的过滤器。它可以对单个路由或者一组路由上传入的请求和传出响应进行拦截,并实现一些与业务无关的功能,比如登陆状态校验、签名校验、权限校验、日志输出、流量监控等。

spring:
  cloud:
    gateway: 
      routes:
        - id: xxxx
          uri: xxxx
          predicates:
            - Path=xxxx
          filters:
            - AddRequestParameter=X-Request-Id,888 #过滤器工厂会在匹配的请求头加上一对请求头,名称为 X-Request-Id 值为 888
            - PrefixPath=/load #在请求路径前面加上 /load

 Spring Cloud Gateway 内置了多达 31 种 GatewayFilter,如下列举了几种常用的网关过滤器。

路由过滤器描述参数使用示例
AddRequestHeader  拦截传入的请求,并在请求上添加一个指定的请求头参数。 name:需要添加的请求头参数的 key;
value:需要添加的请求头参数的 value。
- AddRequestHeader=my-request-header,1024
AddRequestParameter 拦截传入的请求,并在请求上添加一个指定的请求参数。 name:需要添加的请求参数的 key;
value:需要添加的请求参数的 value。
- AddRequestParameter=name,augus
AddResponseHeader 拦截响应,并在响应上添加一个指定的响应头参数。 name:需要添加的响应头的 key;
value:需要添加的响应头的 value。
- AddResponseHeader=respheader,hello
PrefixPath 拦截传入的请求,并在请求路径增加一个指定的前缀。  prefix:需要增加的路径前缀。 - PrefixPath=/consumer
PreserveHostHeader 转发请求时,保持客户端的 Host 信息不变,然后将它传递到提供具体服务的微服务中。 - PreserveHostHeader
RemoveRequestHeader 移除请求头中指定的参数。 name:需要移除的请求头的 key。 - RemoveRequestHeader=requestheader
RemoveResponseHeader 移除响应头中指定的参数。 name:需要移除的响应头。 - RemoveResponseHeader=respheader
RemoveRequestParameter 移除指定的请求参数。 name:需要移除的请求参数。 - RemoveRequestParameter=paramName
RequestSize 配置请求体的大小,当请求体过大时,将会返回 413 Payload Too Large。 maxSize:请求体的大小。 - name: RequestSize
   args:
     maxSize: 5000000

案例演示:

  • 在9527的application.yml中给查询接口添加请求头

对于访问 http://localhost:9527/payment/get/1 设置一个过滤器添加一个请求头,请求头内容为:

filters:
- AddRequestHeader=info,'default filers request header!' #增加一个请求头名为info值为default filers request header!

如下图所示

  •  修改8001和8002中PaymentController中 http://localhost:9527/payment/get/1 接口方:

代码如下:这里主要是为了获取上面的过滤器添加的请求头信息,注意由于做了集群,所以8001和8002 都需要添加

@GetMapping(value = "/payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id") Long id, @RequestHeader(value = "Info",required = false) String info){

        //输出获取的请求头
        System.out.println("**************info:"+info);
        Payment payment = paymentService.queryById(id);
        if (payment!=null){
            return new CommonResult(200,"查询成功serverPort:"+serverPort, payment);
        }else{
            return new CommonResult(400,"查无记录");
        }
    }
  • 测试

先重启8001和8002,然后在启动9527,之后如图:

然后去8002服务对应的控制台,会打印出本次访问添加的请求头信息,如图所示:

7.3.GlobalFilter 全局过滤器

GlobalFilter 是一种作用于所有的路由上的全局过滤器,通过它,我们可以实现一些统一化的业务功能,例如权限认证、IP 访问限制等。当某个请求被路由匹配时,那么所有的 GlobalFilter 会和该路由自身配置的 GatewayFilter 组合成一个过滤器链。
Spring Cloud Gateway 为我们提供了多种默认的 GlobalFilter,例如与转发、路由、负载均衡等相关的全局过滤器。但在实际的项目开发中,通常我们都会自定义一些自己的 GlobalFilter 全局过滤器以满足我们自身的业务需求,而很少直接使用 Spring Cloud  Config 提供这些默认的 GlobalFilter。案例如下:

7.3.1. 在 9527Gateway 服务下的 com.augus.cloud.filter 包下,新建一个名为 MyGlobalFilter 全局过滤器配置类:

在下面设置全局过滤器,对这个网关中每一个请求都是有效的,下面设置的是所有请求都需要带一个username的参数,才能成功访问

package com.augus.cloud.filter;

import lombok.extern.slf4j.Slf4j;
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.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Date;

/**
 * 自定义全局过滤器
 */
@Component
@Slf4j
public class MyGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("进入自定义的全局过滤器名为:MyGlobalFilter", new Date());
        String username = exchange.getRequest().getQueryParams().getFirst("username");
        //判断传过去的参数是否为空
        if(username == null){
            log.info("参数username不能为null");
            //为空则响应设置状态码 为406HttpStatus.NOT_ACCEPTABLE
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

    /**
     * 过滤器加载的顺序越小,优先级别越高
     * @return
     */
    @Override
    public int getOrder() {
        //过滤器的顺序,0表示第一个
        return 0;
    }
}

7.3.2. 重启9527服务

重启服务9527,直接访问http://localhost:9527/payment/get/1,如图所示无法查询出来内容:

然后访问:http://localhost:9527/payment/get/1?username=123,这时候带上参数就可以正常访问,这个就是上面全局过滤器的设置后的效果,如下图:

 

posted @ 2023-01-10 17:12  酒剑仙*  阅读(248)  评论(0)    收藏  举报