前言

什么是网关

在复杂的微服务架构中,一个业务功能可能由多个微服务协同完成,每个微服务都有自己独立的网络地址。如果没有网关,客户端需要直接与各个微服务进行交互,这会带来一系列问题:

  • 客户端复杂性增加: 客户端需要记录每个微服务的地址,并处理复杂的调用逻辑,增加了客户端的开发和维护成本。
  • 认证授权复杂: 每个微服务都需要进行独立的认证和授权,导致重复开发和管理困难
  • 跨域问题: 不同域的服务之间调用会存在跨域请求的问题,处理起来较为繁琐。
  • 难以实现统一的非业务功能: 像日志记录、限流、熔断、监控等通用功能难以在每个微服务中统一实现和管理。

为了解决上述问题,API网关应运而生。它作为系统的唯一入口,封装了内部的系统架构,为客户端提供一个统一的、简化的API。也就是

一切访问都要通过网关转发给微服务

Spring Cloud Gateway 模块介绍

在微服务架构中,API网关扮演着至关重要的角色,它作为所有微服务的前置入口,统一处理客户端请求,并将其路由到相应的后端服务。Spring Cloud Gateway 作为 Spring Cloud 生态系统中的新一代网关,凭借其强大的功能和高性能,成为了构建微服务架构的首选组件之一。

Spring Cloud Gateway是Spring Cloud生态中的第二代网关,旨在替代Netflix Zuul,它基于Spring Framework 5、Spring Boot 2和Project Reactor构建,采用了非阻塞的WebFlux框架,因此拥有出色的性能和可扩展性。

Spring Cloud Gateway的核心概念主要包括:

  • 路由 (Route): 路由是网关的基本构建模块,由一个ID、一个目标URI、一组断言和一组过滤器组成。 当请求满足所有断言时,该路由将被匹配,请求会被转发到目标URI。
  • 断言 (Predicate): 断言是Java 8的函数式接口,用于匹配HTTP请求中的任何内容,例如请求路径、请求头、请求参数等。如果请求与断言匹配,则路由生效。
  • 过滤器 (Filter): 过滤器用于在请求被路由之前或之后对请求进行修改。Spring Cloud Gateway提供了丰富的内置过滤器,也支持自定义过滤器,可以用来实现鉴权、限流、日志记录等功能。过滤器的生命周期分为“pre”和“post”两个阶段,“pre”过滤器在请求被路由前执行,“post”过滤器在路由到微服务后执行。

Spring Cloud Gateway 的作用

在微服务架构中,使用像Spring Cloud Gateway这样的API网关是至关重要的。它的作用体现在以下几个方面:

  • 简化客户端开发和降低耦合度: 网关为所有微服务提供了一个统一的入口,客户端只需与网关进行交互,无需关心后端复杂的微服务拓扑结构。这大大简化了客户端的开发,并降低了客户端与后端服务的耦合度。

  • 提升系统安全性和可维护性: 通过在网关层进行统一的认证和鉴权,可以有效地保护后端服务。 同时,将日志、监控等非业务功能集中在网关处理,也降低了每个微服务的开发和维护成本。

  • 高性能的异步非阻塞模型: Spring Cloud Gateway基于WebFlux实现,采用异步非阻塞的编程模型,能够以较少的线程处理大量的并发请求,在高并发场景下表现出色。这也是它相较于第一代网关Zuul 1.x的主要优势之一。

  • 动态路由和灵活的配置: Spring Cloud Gateway支持动态路由,可以根据服务的实例变化自动更新路由信息。[] 同时,其丰富的断言和过滤器可以灵活地满足各种复杂的路由和请求处理需求。

统一网关和配置中心的联系

而为了实现更灵活和动态的管理,Spring Cloud Gateway通常会与配置中心(如Nacos、Spring Cloud Config等)进行集成。这种集成主要体现在以下几个方面:

  • 动态路由配置: 我们可以将网关的路由规则配置在配置中心,而不是硬编码在代码或本地配置文件中。[][] 这样,当需要新增、修改或删除路由规则时,只需要在配置中心进行操作,网关就能够动态地获取最新的配置并生效,无需重启服务。这极大地提高了系统的灵活性和可维护性。
  • 服务发现与负载均衡: Spring Cloud Gateway可以与服务注册中心(通常也由Nacos等组件提供)集成,从而实现服务的动态发现。当一个新的微服务实例注册到注册中心时,网关能够自动感知并将其加入到路由的目标列表中。结合负载均衡组件,网关可以将请求分发到不同的服务实例上,实现动态的负载均衡。
  • 统一的配置管理: 除了路由规则,网关的其他配置,例如限流规则、过滤器配置等,也可以存储在配置中心进行统一管理。这样可以方便地对整个微服务系统的配置进行集中化管理和版本控制。

将Spring Cloud Gateway与Nacos整合,可以实现服务注册、服务发现和动态配置。在这种架构下:

  1. 各个微服务实例启动后会向Nacos注册中心注册自己的信息。
  2. Spring Cloud Gateway从Nacos获取所有可用的服务实例列表。
  3. 当客户端请求到达网关时,网关会根据配置在Nacos中的路由规则,将请求转发到相应的服务实例上
  4. 如果路由规则或服务实例信息发生变化,Nacos会通知网关,网关会动态更新其路由表,从而实现路由的动态更新。

搭建统一网关服务的各种知识

搭建网关并注册服务的步骤

  1. 创建新的模块,引入 Spring Cloud Gateway 的依赖和 Nacos 服务发现的依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
     <!-- Spring Cloud Gateway -->
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <!-- Spring Cloud Alibaba Nacos Discovery -->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>

    这两个依赖是网关模块必备的

  2. 编写路由配置和 Nacos 地址,我举例一些常用的配置项目

    网关路由可以配置的内容包括:

    • 路由id:路由唯一标示
    • uri: 路由目的地,支持lb和http两种
    • predicates: 路由断言,判断请求是否符合要求,符合则转发到路由目的地
    • filters:路由过滤器,处理请求或响应
    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
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    # 路由配置 - 定义一个路由规则,ID为example-route
    spring.cloud.gateway.routes[0].id=example-route
    # 路由目标地址 - 将请求转发到http://example.com
    spring.cloud.gateway.routes[0].uri=http://example.com

    # 这是路由断言。也就是判断请求是否符合路由规则的条件
    # 路由谓词 - 匹配路径以/api/开头的请求
    spring.cloud.gateway.routes[0].predicates[0]=Path=/api/**
    # 路由谓词 - 只匹配GET和POST方法的请求
    spring.cloud.gateway.routes[0].predicates[1]=Method=GET,POST
    # 路由过滤器 - 重写路径,将/api/xxx重写为/xxx
    spring.cloud.gateway.routes[0].filters[0]=RewritePath=/api/(?<segment>.*),/$\{segment}
    # 路由过滤器 - 向请求添加头信息X-Request-Source: gateway
    spring.cloud.gateway.routes[0].filters[1]=AddRequestHeader=X-Request-Source, gateway
    # 路由优先级 - 数值越小优先级越高
    spring.cloud.gateway.routes[0].order=0

    # 全局过滤器配置 - 对所有路由生效的过滤器
    # 向所有响应添加头信息X-Response-Source: gateway
    spring.cloud.gateway.default-filters[0]=AddResponseHeader=X-Response-Source, gateway

    # 超时配置 - 连接超时时间,单位:毫秒
    spring.cloud.gateway.httpclient.connect-timeout=1000
    # 超时配置 - 响应超时时间,单位:毫秒
    spring.cloud.gateway.httpclient.response-timeout=5000

    # 连接池配置 - 最大连接数
    spring.cloud.gateway.httpclient.pool.max-connections=200
    # 连接池配置 - 获取连接的超时时间,单位:毫秒
    spring.cloud.gateway.httpclient.pool.acquire-timeout=3000

    # 跨域配置 - 允许所有来源访问
    spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-origins=*
    # 跨域配置 - 允许的HTTP方法
    spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-methods=GET,POST,PUT,DELETE
    # 跨域配置 - 允许的请求头
    spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-headers=*
    # 跨域配置 - 是否允许携带凭证(cookie)
    spring.cloud.gateway.globalcors.cors-configurations.[/**].allow-credentials=true

    # 负载均衡配置 - 启用服务发现定位器
    spring.cloud.gateway.discovery.locator.enabled=true
    # 负载均衡配置 - 服务ID转为小写(适配某些注册中心)
    spring.cloud.gateway.discovery.locator.lower-case-service-id=true

    # 路由缓存配置 - 路由定义的缓存时间,单位:毫秒
    spring.cloud.gateway.route-cachettl=30000

    # 服务发现路由配置 - 定义用户服务路由,ID为user-service
    spring.cloud.gateway.routes[1].id=user-service
    # 服务发现路由配置 - 转发到名为user-service的服务(lb表示负载均衡),当然你也可以写死地址
    spring.cloud.gateway.routes[1].uri=lb://user-service
    # 服务发现路由配置 - 匹配路径以/user/开头的请求
    spring.cloud.gateway.routes[1].predicates[0]=Path=/user/**
    # 服务发现路由配置 - 移除路径前缀(将/user/xxx转发为/xxx)
    spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1

    # 熔断配置 - 使用CircuitBreaker过滤器
    spring.cloud.gateway.routes[2].filters[0]=name=CircuitBreaker
    # 熔断配置 - 熔断器名称
    spring.cloud.gateway.routes[2].filters[0].args.name=myCircuitBreaker
    # 熔断配置 - 熔断时的降级路径
    spring.cloud.gateway.routes[2].filters[0].args.fallbackUri=forward:/fallback

    # 重试配置 - 使用Retry过滤器
    spring.cloud.gateway.routes[3].filters[0]=name=Retry
    # 重试配置 - 最大重试次数
    spring.cloud.gateway.routes[3].filters[0].args.retries=3
    # 重试配置 - 需要重试的HTTP状态码
    spring.cloud.gateway.routes[3].filters[0].args.statuses=BAD_GATEWAY, SERVICE_UNAVAILABLE

    路由断言,路由发现配置,这些是必须写的

  3. 现在我们需要把需要的服务注册到Nacos中,这样就能通过服务发现被网关发现,别忘了在需要被注册的服务的主类上添加 @EnableDiscoveryClient 注解,使其成为一个 Nacos 客户端。

    以配置一个 product-service 为例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # application.yml
    server:
    port: 9001 # 为商品服务分配一个端口
    spring:
    application:
    name: product-service # 应用名称,这是服务发现的关键
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 # Nacos服务器地址
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    # bootstrap.yml
    server:
    port: 8888 # 为网关服务分配一个端口
    spring:
    application:
    name: gateway-service # 网关应用名称
    cloud:
    nacos:
    discovery:
    server-addr: localhost:8848 # Nacos服务发现地址
    config:
    server-addr: localhost:8848 # Nacos配置中心地址
    file-extension: yaml # 指定配置文件的格式为yaml
    # group: DEFAULT_GROUP # 可以指定配置分组
  4. 在 Nacos 中配置动态路由,这是实现“统一网关”的核心步骤。我们将路由规则集中存储在 Nacos 中,而不是写死在网关服务的配置文件里。

    • 登录 Nacos 控制台 http://localhost:8848/nacos ,进入 配置管理 -> 配置列表

    • 点击右上角的 “+” 号创建新配置。填写配置信息:

      • Data ID: gateway-service.yaml (这个名称必须与网关服务的 spring.application.name 和 file-extension 匹配)。

      • Group: DEFAULT_GROUP (保持默认即可)。

      • 配置格式: YAML

      • 配置内容

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        spring:
        cloud:
        gateway:
        # 开启通过服务发现来路由的功能
        discovery:
        locator:
        enabled: true
        # 定义具体的路由规则
        routes:
        # 路由规则ID,要求唯一
        - id: product_route
        # 目标URI,lb:// 表示从Nacos服务列表中获取服务,并进行负载均衡
        # product-service 是后端微服务的 spring.application.name
        uri: lb://product-service
        # 断言(Predicates),即路由的匹配条件
        predicates:
        # 当请求路径匹配 /api/product/** 时,此路由生效
        - Path=/api/product/**
        # 过滤器(Filters),在请求转发前后进行处理
        filters:
        # 移除请求路径的第一段 (/api),再转发给后端服务
        # 例如,请求 /api/product/1 会被转发到 product-service 的 /product/1
        - StripPrefix=1

        点击 “发布”,配置即刻生效。

  5. 到这里就基本完成统一网关的搭建和服务注册的步骤了,最后一步就是进行测试了

路由断言工厂

网关路由我们主要配置的项目包括:

  • 路由id:路由唯一标示
  • uri:路由的目的地,支持lb
  • perdicates:路由断言,判断请求是否符合要求,符合则转发到目的地
  • filters:路由的过滤器,处理和请求响应

Spring Cloud Gateway作为新一代的API网关,其核心功能之一就是路由。而要让网关知道如何将一个进来的请求正确地转发到后端的某个微服务,就需要一套规则,这就是路由断言(Predicate) 的作用。

路由断言工厂是什么?简单来说,路由断言工厂就是用来创建路由断言(Predicate) 的工厂。在Spring Cloud Gateway中,这个输入就是HTTP请求的所有信息(封装在ServerWebExchange对象中)。

当一个请求到达网关时,网关会取出所有的路由规则,并使用与该路由关联的Predicate来判断这个请求是否满足条件。如果满足(即Predicate返回true),那么这个路由就被选中,请求就会被转发到该路由指定的目标URI。

在Spring Cloud Gateway项目中,路由断言的使用主要分为两类:使用内置断言工厂自定义断言工厂

而我们在配置文件中写的断言规则字符串,会被 Predicate Factory 处理,转为路由判断的条件,所以我们绕过提供的路由断言工厂自己来一套新的断言工厂就是自定义路由断言工厂。

Spring Cloud Gateway提供了丰富的内置断言工厂,足以满足绝大多数路由场景。它们都遵循一个约定,在配置文件中,工厂的名称就是其类名去掉RoutePredicateFactory后缀。例如,PathRoutePredicateFactory在YAML中就写成Path

image-20250725155810043

如果有很多个Predicate,并且一个请求满足多个Predicate,则按照配置的顺序第一个生效。

请注意:一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发。

我只列举一些常用的

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
spring:
cloud:
gateway:
routes:
# 规则一:基于路径匹配 (Path)
- id: path_route
uri: lb://user-service # lb代表从注册中心进行负载均衡
predicates:
- Path=/users/** # 匹配所有以 /users/ 开头的请求

# 规则二:基于请求方法匹配 (Method)
- id: method_route
uri: lb://order-service
predicates:
- Method=GET # 仅匹配GET方法的请求

# 规则三:基于请求头匹配 (Header)
- id: header_route
uri: lb://product-service-v2
predicates:
- Header=X-Api-Version, v2 # 匹配请求头中包含 X-Api-Version: v2 的请求

# 规则四:基于查询参数匹配 (Query)
- id: query_route
uri: lb://search-service
predicates:
- Query=keyword, . # 匹配包含名为keyword的查询参数的请求,第二个参数是正则表达式

# 规则五:基于Cookie匹配 (Cookie)
- id: cookie_route
uri: lb://vip-service
predicates:
- Cookie=user_level, vip # 匹配Cookie中包含 user_level=vip 的请求

# 规则六:基于时间匹配 (Between)
- id: between_route
uri: lb://activity-service
predicates:
# 匹配在指定时间段内的请求(注意时区)
- Between=2025-08-08T10:00:00+08:00[Asia/Shanghai], 2025-08-08T12:00:00+08:00[Asia/Shanghai]

# 多个断言组合使用(逻辑与关系)
- id: combined_route
uri: lb://payment-service
predicates:
- Path=/payment/**
- Header=X-Client-Type, mobile # 必须同时满足路径和请求头的条件

当内置的断言无法满足复杂的业务逻辑时,可以自定义断言工厂。

自定义断言工厂需要继承AbstractRoutePredicateFactory<C>,其中C是用于接收配置参数的配置类。

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
57
58
@Component // 必须注册为Spring Bean
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {

// 构造函数,传入配置类
public AgeRoutePredicateFactory() {
super(Config.class);
}

// 读取配置文件中的参数值,并将其绑定到Config类的属性上
@Override
public List<String> shortcutFieldOrder() {
// 这里的顺序必须和配置文件中的值的顺序一致
return Arrays.asList("minAge", "maxAge");
}

// 核心的断言逻辑
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
// 从请求中获取名为 "age" 的参数
String ageStr = exchange.getRequest().getQueryParams().getFirst("age");
if (ageStr != null) {
try {
int age = Integer.parseInt(ageStr);
// 判断年龄是否在配置的范围内
return age >= config.getMinAge() && age < config.getMaxAge();
} catch (NumberFormatException e) {
return false;
}
}
return false;
};
}

// 静态内部类,用于接收和存储配置文件中的参数
public static class Config {
@NotNull
private int minAge; // 最小年龄
@NotNull
private int maxAge; // 最大年龄

public int getMinAge() {
return minAge;
}

public void setMinAge(int minAge) {
this.minAge = minAge;
}

public int getMaxAge() {
return maxAge;
}

public void setMaxAge(int maxAge) {
this.maxAge = maxAge;
}
}
}

application.yml中使用自定义断言,工厂的名称是类名AgeRoutePredicateFactory去掉后缀,即Age

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: age_check_route
uri: lb://some-sensitive-service
predicates:
- Path=/sensitive-data/**
# 使用自定义的Age断言工厂
# 参数会按shortcutFieldOrder()定义的顺序赋值给Config对象
# 18对应minAge, 60对应maxAge
- Age=18, 60

现在,当一个请求如http://localhost:8080/sensitive-data/info?age=25到达网关时,Age断言会生效,判断 25 在 [18, 60)区间内,返回true,请求被成功路由。如果 age 为 17 或 65,则断言失败,该路由不匹配。

注意,无法路由是 404 错误,不是 5xx

配置路由的过滤器

什么是路由过滤器

GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理

它决定了在一个匹配的请求被转发之前之后,应该对这个请求做什么样的处理。

image-20250725160755273

路由过滤器是针对单个路由规则生效的过滤器。它与特定的路由绑定,只有当请求匹配了该路由的断言条件后,这个过滤器才会被执行。

一般在在微服务架构中,路由过滤器是实现各种横切关注点和流量控制的要点,以下是常用的路由过滤器

image-20250726123327162

添加请求头 / 响应头过滤器

用于在请求发送到下游服务前添加请求头,或在响应返回给客户端前添加响应头。

  • AddRequestHeader 给请求添加指定的请求头。 示例:AddRequestHeader=X-Request-Source, gateway 作用:所有匹配该路由的请求都会添加 X-Request-Source: gateway 头。
  • AddResponseHeader 给响应添加指定的响应头。 示例:AddResponseHeader=X-Response-Time, ${time} 作用:响应返回时添加 X-Response-Time 头(可结合变量动态赋值)。
  • SetRequestHeader 覆盖请求中已存在的指定头(若不存在则添加)。 示例:SetRequestHeader=Content-Type, application/json
  • SetResponseHeader 覆盖响应中已存在的指定头(若不存在则添加)。

路径修改过滤器

用于重写或调整请求路径,实现路径映射。

  • RewritePath 基于正则表达式重写请求路径。 示例:RewritePath=/api/(?<segment>.*), /$\{segment} 作用:将 /api/user/1 重写为 /user/1segment 是正则分组变量)。
  • StripPrefix 移除路径中的指定前缀层数。 示例:StripPrefix=1 作用:若请求路径为 /user/api/detail,移除第一层前缀后变为 /api/detail
  • PrefixPath 给路径添加指定前缀。 示例:PrefixPath=/api 作用:将 /user 转换为 /api/user

参数处理过滤器

用于添加、修改或移除请求参数。

  • AddRequestParameter 给请求添加查询参数。 示例:AddRequestParameter=version, v1 作用:所有请求会自动带上 ?version=v1 参数。
  • SetRequestParameter 覆盖已存在的查询参数(若不存在则添加)。 示例:SetRequestParameter=type, mobile

重定向过滤器

用于将请求重定向到其他地址。

  • RedirectTo 重定向到指定 URL,需指定状态码(如 302 临时重定向)。 示例:RedirectTo=302, https://example.com 作用:将匹配的请求重定向到 https://example.com

熔断与降级过滤器

结合 Spring Cloud Circuit Breaker 实现熔断降级(需引入对应依赖,如 Resilience4j)。

  • CircuitBreaker 对路由启用熔断机制,当服务异常时触发降级。

重试过滤器

当请求失败时自动重试,可配置重试条件。

  • Retry 配置重试次数、重试状态码等。

限速过滤器

用于限制请求频率,防止服务过载(需结合限流器,如 Redis)。

  • RequestRateLimiter 基于令牌桶算法实现限流。

安全相关过滤器

  • RemoveRequestHeader 移除请求中的指定头(如敏感信息)。 示例:RemoveRequestHeader=Authorization
  • RemoveResponseHeader 移除响应中的指定头(如隐藏服务信息)。 示例:RemoveResponseHeader=X-Powered-By

其他常用过滤器

  • DedupeResponseHeader 去重响应头(避免下游服务返回重复头)。 示例:DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_FIRST
  • RewriteResponseHeader 重写响应头内容(基于正则)。

配置路由过滤器

在application.yml中,路由过滤器的配置位于对应路由规则的filters节点下,可以配置多个。

内置路由过滤器配置示例:

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
spring:
cloud:
gateway:
routes:
# 规则一:重写路径 (RewritePath)
- id: rewrite_path_route
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
# 将 /api/users/123 重写为 /users/123
- RewritePath=/api/(?<segment>.*), /$\{segment}

# 规则二:剥离前缀 (StripPrefix)
- id: strip_prefix_route
uri: lb://product-service
predicates:
- Path=/products/**
filters:
# 将 /products/item/1 剥离掉1个前缀,变成 /item/1
- StripPrefix=1

# 规则三:添加请求头 (AddRequestHeader)
- id: add_header_route
uri: lb://order-service
predicates:
- Path=/my-orders/**
filters:
- AddRequestHeader=X-User-ID, 10086 # 假设用户ID是10086

# 规则四:重试 (Retry)
- id: retry_route
uri: lb://inventory-service
predicates:
- Path=/inventory/check
filters:
- name: Retry
args:
retries: 3 # 重试3次
statuses: BAD_GATEWAY, SERVICE_UNAVAILABLE # 只有在这些状态码时才重试
methods: GET, POST # 只有GET和POST方法才重试

# 规则五:请求限流 (RequestRateLimiter)
# 需要额外配置和Redis依赖
- id: rate_limiter_route
uri: lb://payment-service
predicates:
- Path=/payment/create
filters:
- name: RequestRateLimiter
args:
# 使用SpEL表达式,根据用户ID进行限流
key-resolver: "#{@userIdKeyResolver}"
redis-rate-limiter.replenishRate: 1 # 每秒允许1个请求
redis-rate-limiter.burstCapacity: 2 # 令牌桶容量为2

自定义路由过滤器工厂 (GatewayFilterFactory)与自定义断言类似,当内置过滤器不满足需求时,可以自定义。

我们以创建一个记录请求处理时间的过滤器为场景

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
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

@Component
public class ExecutionTimeGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

private static final Logger log = LoggerFactory.getLogger(ExecutionTimeGatewayFilterFactory.class);
private static final String START_TIME = "startTime";

@Override
public GatewayFilter apply(Object config) {
return (exchange, chain) -> {
// Pre-processing: 请求被转发前执行
// 在 exchange 的 attributes 中存入当前时间
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());

// Post-processing: 请求从后端服务返回后执行
return chain.filter(exchange).then(
Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(START_TIME);
if (startTime != null) {
long executionTime = System.currentTimeMillis() - startTime;
log.info("{} 请求 {} 耗时: {}ms",
exchange.getRequest().getURI().getRawPath(),
exchange.getResponse().getStatusCode(),
executionTime);
}
})
);
};
}
}

配置自定义过滤器:在application.yml中,工厂名称是类名去掉GatewayFilterFactory后缀。

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: time_logging_route
uri: lb://some-service
predicates:
- Path=/some/api/**
filters:
# 使用自定义的过滤器
- ExecutionTime

全局过滤器

我们可以对过滤器配置 default-filters,这是默认过滤器,会对所有的请求都生效

image-20250725161530595

但是 GlobalFilter 是专门做这个的,只不过我们上面说的默认过滤器的处理逻辑是固定的,配置哪个用哪个,而 GlobalFilter 的逻辑需要你自己写代码进行实现

全局过滤器非常适合实现系统级的通用功能。

全局过滤器需要实现GlobalFilterOrdered接口,并通过@Component注解注入到Spring容器中。实现一个简单的全局身份认证过滤器为例子

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
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

private static final Logger log = LoggerFactory.getLogger(AuthGlobalFilter.class);

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 白名单路径,直接放行
String path = exchange.getRequest().getURI().getPath();
if (path.contains("/login") || path.contains("/register")) {
return chain.filter(exchange);
}

// 从请求头获取token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");

if (token == null || token.isEmpty()) {
log.warn("认证失败,未找到token");
// 认证失败,设置响应状态码为401
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
// 结束请求处理
return exchange.getResponse().setComplete();
}

// 假设这里有复杂的token校验逻辑...
// if (isTokenInvalid(token)) { ... }

log.info("认证成功,token: {}", token);
// 认证成功,放行到下一个过滤器
return chain.filter(exchange);
}

@Override
public int getOrder() {
// 设置过滤器的执行顺序,值越小,优先级越高
// 认证过滤器通常需要最先执行
return -100;
}
}

这个全局过滤器无需在YAML中配置,只要它被Spring容器管理,就会自动对所有路由生效。getOrder()方法非常重要,它决定了多个全局过滤器的执行顺序。

过滤器的链的执行顺序

省流

WW
image-20250725163454837

不省流

首先要明确,Gateway中的过滤器逻辑上分为两个阶段:

  1. “Pre” 阶段:在请求被路由到下游微服务之前执行的逻辑。这是我们最常用的阶段,可以用于身份认证、请求参数修改、日志记录、限流等。
  2. “Post” 阶段:在下游微服务返回响应之后,响应数据返回给客户端之前执行的逻辑。常用于修改响应头、记录响应状态和处理耗时等。

这种 Pre 和 Post 的划分并不是通过配置两个不同的过滤器来实现的,而是在同一个过滤器的代码逻辑中体现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
return (exchange, chain) -> {
// ===================================
// "Pre" 阶段逻辑在这里执行
// ===================================

// 调用 chain.filter(exchange) 将请求传递给下一个过滤器
// 这行代码是"Pre"和"Post"阶段的分界线
return chain.filter(exchange).then(
// Mono.fromRunnable() 确保在响应完成后执行
Mono.fromRunnable(() -> {
// ===================================
// "Post" 阶段逻辑在这里执行
// ===================================
})
);
};

当一个请求到达Gateway时,它会经过一个由多种过滤器组成的链条。这个链条的整体顺序是固定的,可以想象成一个链条结构:

请求进入 (Request) -> Pre阶段过滤器链 -> 目标微服务 -> Post阶段过滤器链 -> 响应返回 (Response)

这个链条具体由三类过滤器构成,它们的执行优先级从高到低依次是:

  1. 默认过滤器 (Default Filters):由Spring Cloud Gateway框架自身提供,用于实现核心的路由转发、负载均衡等功能。这些过滤器拥有最高的优先级,我们通常不需要关心它们。
  2. 全局过滤器 (GlobalFilter):作用于所有路由的过滤器。
  3. 路由过滤器 (GatewayFilter):只作用于当前匹配路由的过滤器。

排序依据如下,所有过滤器(全局和路由)都会被放在同一个链条里排序。排序的依据是 order 值。order 值越小,优先级越高。

  • “Pre”阶段顺序:在”Pre”阶段,过滤器按照order值从小到大的顺序依次执行。
  • “Post”阶段顺序:在”Post”阶段,顺序则完全相反,按照order值从大到小的顺序依次执行。这就像一个栈(Stack)的“后进先出”(LIFO)行为。最后执行”Pre”逻辑的过滤器,最先执行”Post”逻辑。

全局过滤器的顺序由其实现的getOrder()方法或@Order注解决定。上面说过,但是

当全局过滤器和路由过滤器同时存在时,它们会统一按照 order 值进行排序。

  • 全局过滤器的 order 值通常是我们自己定义的负数或较小正数,以确保它们在路由过滤器之前执行。例如,认证(-100)、日志(-50)。
  • 路由过滤器的 order 值由其在YAML中的声明顺序决定,从1开始递增。

路由过滤器的顺序也由它们在application.yml文件中的声明顺序决定。

在YAML配置中,filters是一个列表(List),Gateway会按照这个列表的从上到下的顺序为它们分配order值。第一个过滤器的order是1,第二个是2,以此类推。

网关的cors跨域配置

回顾 CORS

我们需要彻底理解跨域问题 CORS 是什么?(这里算是私货,因为我学的不好,在这里借机再搞一下)

李鹏飞某一天跟我讲了一个这样的问题

你住在一个叫做 a.com 的小区(这叫源 Origin),这个小区的安保非常严格。有一天,你想点一份来自 b.com 小区的披萨外卖。

浏览器的同源策略(Same-Origin Policy) 就好比你小区的保安。这个保安规定:为了安全,你(a.com 里的网页脚本)默认只能访问你自己小区(a.com)内部的资源,比如小区的公告栏(你自己服务器上的API)。

当你试图去获取 b.com 披萨店(另一个服务器的API)的数据时,保安(浏览器)会拦住你,说:“等等,那不是我们小区的,不安全,不让你拿!” 这就发生了跨域错误

6667,0秒看懂 CORS

一个源由三个部分组成:协议(Protocol)+ 域名(Domain)+ 端口(Port)。只要这三者中有一个不一样,就是不同的源,就会有跨域问题。

CORS(Cross-Origin Resource Sharing,跨域资源共享)就是 b.com 披萨店为了能把外卖送到你手里,主动给你的小区保安出示的一套“通行证”。

这套通行证是通过 HTTP 响应头 来实现的。当 b.com 的服务器收到请求时,它在返回的响应里加上一些特殊的头信息,当浏览器收到这个带有“通行证”的响应后,就会放行,你就能成功拿到披萨了。

对于一些“复杂”的请求(比如 PUT、DELETE 方法,或者带有一些特殊请求头的请求),浏览器会更谨慎。它会先自动发送一个 OPTIONS 方法的预检请求去问一下 b.com 的服务器:“我待会儿准备用 PUT 方法带个 X-Token 头来请求,你那边允许吗?”

b.com 服务器如果允许,就在预检请求的响应中返回上述的CORS头信息。浏览器确认“通行证”有效后,才会发送真正的 PUT 请求。这就是为什么有时候你在浏览器开发者工具里会看到一个 OPTIONS 请求失败,然后真正的请求根本没发出去。

注意,跨域是浏览器的安全策略,不是服务器的错误,也不是框架的规定。

在你的统一网关搭建服务中实现跨域请求

在微服务架构中,你有 user-service、product-service、order-service 等很多个后端服务。前端应用可能需要同时调用这三个服务。

总不能三个都配一次吧,这时候网关层面的配置就出手了

Spring Cloud Gateway 提供了非常便捷的全局CORS配置方式。强烈推荐使用这种方式。

通过 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
26
27
28
29
30
31
32
spring:
cloud:
gateway:
# ... 其他路由配置 ...

# 全局CORS跨域配置
globalcors:
cors-configurations:
# '[/**]' 是一个路径匹配模式,表示对所有路径都应用这个CORS配置
'[/**]':
# 允许跨域的源,可以用 * 代表所有,但生产环境不推荐,应指定具体的域名
# allowedOrigins: "*"
allowedOrigins:
- "http://localhost:8081" # 允许本地的前端项目
- "http://www.your-frontend.com" # 允许生产环境的前端项目

# 允许的请求方法,* 代表所有
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS

# 允许携带的请求头,* 代表所有
allowedHeaders: "*"

# 是否允许携带Cookie等凭证
allowCredentials: true

# 预检请求的缓存时间(秒),在此时间内,浏览器无需为同一请求再次发送预检请求
maxAge: 3600

说一下,'[/**]'[ ]是YAML语法,表示这是一个Key。/** 是路径匹配符,代表拦截所有请求,对它们统一应用下面的CORS规则。

网关异常的统一处理

https://blog.csdn.net/weixin_44863237/article/details/134728229,感谢

我们在通过gateway进行路由转发时,可能会遇到各种的问题,通过统一异常处理器,可以将不同的异常进行集中式处理和管理。在开发或者使用过程中报错时,可以更加清晰明了的发现错误提示信息,方便更快地理解和解决问题,还可以优化用户体验并降低维护成本。

自定义异常处理器方式: 可以通过实现ErrorWebExceptionHandler接口来自定义异常处理器,对全局异常进行统一处理。 在这个处理器中,可以根据异常的类型、状态码等信息,返回不同的响应结果给前端。

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
57
58
59
60
/**
* 网关统一异常处理
*/
@Order(-1)
@Configuration
public class GatewayExceptionHandler implements ErrorWebExceptionHandler
{
private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class);

@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
{
ServerHttpResponse response = exchange.getResponse();

# 首先检查响应是否已经提交(即是否已经发送给客户端)。如果已提交,则直接返回包含异常的Mono。
if (exchange.getResponse().isCommitted())
{
return Mono.error(ex);
}

String msg = "";

if (ex instanceof NotFoundException)
{
msg = "服务器迷路啦,30秒后再重试一下. O(∩_∩)O ";
}
else if (ex instanceof ResponseStatusException)
{
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
msg = responseStatusException.getMessage();
}
else
{
msg = "内部服务器异常,请联系管理员";
}

log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());

return webFluxResponseWriter(response, MediaType.APPLICATION_JSON_VALUE, HttpStatus.OK, msg);
}

/**
* 设置webflux模型响应
*
* @param response ServerHttpResponse
* @param contentType content-type
* @param status http状态码
* @param value 响应内容
* @return Mono<Void>
*/
public static Mono<Void> webFluxResponseWriter(ServerHttpResponse response, String contentType, HttpStatus status, Object value)
{
response.setStatusCode(status);
response.getHeaders().add(HttpHeaders.CONTENT_TYPE, contentType);
# 通用的响应实体类,使用自己定义的通用返回类就行
ResultEntity<?> result = ResultEntity.failed(value.toString());
DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONString(result).getBytes());
return response.writeWith(Mono.just(dataBuffer));
}
}

统一网关搭建微服务模块的实例演示

基础环境搭建

首先我们创建一个 gateway 模块,处理网关的请求

网关模块加入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<!-- Spring Cloud Alibaba Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!-- Spring Cloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

启动类上添加服务发现的注解

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
public class CloudGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(CloudGatewayApplication.class, args);
System.out.println("网关服务启动成功,6667,这个入开了");
}
}

编写配置文件,先编写 bootstrap,对了这个也要加依赖,我上面忘了,自己加一下,然后在这里进行 Naocs 的相关配置

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
# bootstrap.yml 比 application.yml 优先加载,用于配置Nacos配置中心等固定的外部化配置

# 服务基本信息配置
spring:
application:
# 服务名称,与注册中心中的服务名一致
name: cloudGateway

# 配置文件相关设置
profiles:
# 激活的环境,可选dev、test、prod等
active: dev

# Spring Cloud相关配置
cloud:
# Nacos 配置中心配置
nacos:
# 配置中心配置
config:
# Nacos服务器地址,开发环境通常是本机,生产环境需修改为实际地址
server-addr: 127.0.0.1:8848
# 配置文件的文件扩展名,目前支持properties和yaml(yml)
file-extension: yaml
# 配置分组,不同环境可以使用不同的配置分组,默认为DEFAULT_GROUP
group: DEFAULT_GROUP
# 命名空间,用于进一步区分不同环境,默认为public
namespace: 7d31e4b6-cfe4-4045-973b-f91847c06442
# 共享配置文件,可以多个服务共享,格式为dataId的完整配置,多个用逗号隔开
shared-configs:
- data-id: common-config.yml # 共享配置文件名称
group: DEFAULT_GROUP # 配置文件所属分组
refresh: true # 是否支持配置动态刷新

# 服务发现配置
discovery:
# Nacos服务器地址,通常与配置中心一致
server-addr: 127.0.0.1:8848
# 命名空间
namespace: 7d31e4b6-cfe4-4045-973b-f91847c06442
# 服务所属分组
group: DEFAULT_GROUP

# 配置Bootstrap上下文加载及优先级
# 在新版本Spring Cloud中,bootstrap上下文默认不加载,需要确保依赖中有spring-cloud-starter-bootstrap

启动模块,看看能不能发现

image-20250725171408448

我们拿出之前写好的 cloud-product 模块拿出来鼓捣,你可能需要自己写一个控制器,然后按照我的方式进行一步步的配置操作

现在开始进行网关相关的配置了

首先是跨域配置,我直接允许全部了,反正是自己鼓捣玩

1
2
3
4
5
6
7
8
9
10
11
12
13
cloud:
gateway:
# 跨域配置
globalcors:
cors-configurations:
'[/**]': # 对所有请求路径生效
# 不能同时设置allow-credentials为true和allowed-origins为*
# 使用allowed-origin-patterns替代
allowed-origin-patterns: "*" # 允许的来源模式
allowed-methods: "*" # 允许的HTTP方法
allowed-headers: "*" # 允许的头信息
allow-credentials: true # 允许携带凭证
max-age: 3600 # 预检请求的有效期,单位为秒

按照我的 cloud-product 模块,进行如下路由配置

1
2
3
4
5
6
7
8
9
# 路由配置
routes:
# 商品服务路由
- id: product-service-route
uri: lb://cloudProduct
predicates:
- Path=/product/**
filters:
- StripPrefix=0 # 不移除前缀,因为服务本身已配置了context-path

讲一下这些内容为什么要这么写

  • 路由id:命名规范通常是:{服务名}-service-route或{功能}-route,在一个网关中,每个路由ID必须唯一

  • 目标uri:uri定义了请求将被转发到的目标地址,lb://前缀表示使用负载均衡(Load Balancer),cloudProduct是在Nacos注册中心注册的服务名称

    • 对于微服务架构,通常使用lb://服务名格式,让网关通过服务发现机制找到目标服务

    • 服务名必须与目标服务在注册中心(如Nacos)中注册的名称完全一致(区分大小写)

  • 断言 predicates:Path=/product/**表示所有以/product/开头的请求都会被该路由处理 **是通配符,表示匹配任意子路径实际开发中如何配置,根据服务的访问路径模式来配置,通常与服务的context-path保持一致

日志配置

1
2
3
4
5
6
7
8
# 日志配置
logging:
level:
root: INFO # 根日志级别
org.springframework.cloud.gateway: DEBUG # Gateway框架日志级别
org.springframework.http.server.reactive: DEBUG # HTTP服务器反应式日志级别
org.springframework.web.reactive: DEBUG # Web反应式日志级别
reactor.netty: DEBUG # Netty反应式日志级别

别的配置自己看情况加

接下来在 Nacos 中配置 网关配置的 相关内容,别找错命名空间

image-20250725172912814

把你的 application.yml 复制一下进行发布

到这里我们启动项目,测试一下能不能通过网关发送请求

image-20250725172954389

成功了,前置内容我们已经完成,接下来我们进行上面讲的内容的各种实操

配置路由断言及其自定义路由断言工厂

网关服务一个主要作用就是路由配置,通过不同的路由配置实现相应的功能。我们一般需要自定义的有这三种组件

  • Route:路由是网关的基本构件。它由ID、目标URI、路由断言工厂(谓语动词)和过滤器集合定义。如果断言工厂判断为真,则匹配路由。
  • Predicate:路由断言工厂,参照Java8的新特性Predicate。这允许开发人员匹配HTTP请求中的任何内容,比如请求头或参数。
  • Filter:路由过滤器。可以在发送下游请求之前或之后修改请求和响应。

配置 Spring Cloud Gateway 自带的路由断言配置,只需要在 application 配置类中加入

1
2
3
4
5
cloud:
gateway:
routes:
predicates:
- 你的断言配置

我们在这里主要是讲解自定义路由断言工厂的内容,以常用的根据请求头中键值对判断是否匹配路由,因为在微服务架构中,我们常常通过请求头来传递租户信息、客户端版本等,并希望根据这些信息将请求路由到不同的服务实例,我创建的三个类分别如下

  • CustomHeaderRoutePredicateFactory: 自定义请求头断言工厂

    它必须以 RoutePredicateFactory 结尾,这样 Spring Cloud Gateway 才能正确识别和加载它。

    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
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    package edu.software.ergoutree.cloudgateway.predicate;

    /**
    * 自定义请求头断言工厂
    * 用于根据请求头中的键值对判断是否匹配路由
    * 类名必须以 "RoutePredicateFactory" 结尾
    */
    @Component
    public class CustomHeaderRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomHeaderRoutePredicateFactory.Config> {

    /**
    * 配置属性的键,用于在配置文件中引用
    */
    public static final String HEADER_KEY = "headerKey";
    public static final String HEADER_VALUE = "headerValue";

    public CustomHeaderRoutePredicateFactory() {
    super(Config.class);
    }

    /**
    * 定义快捷配置的参数顺序。
    * 当使用 "CustomHeader=X-Client-Version,v2" 这样的快捷方式时,
    * Spring Cloud Gateway 会按此顺序将 v2 赋值给 headerValue。
    */
    @Override
    public List<String> shortcutFieldOrder() {
    return Arrays.asList(HEADER_KEY, HEADER_VALUE);
    }

    /**
    * 核心断言逻辑
    */
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
    return new GatewayPredicate() {
    @Override
    public boolean test(ServerWebExchange exchange) {
    // 1. 从交换上下文中获取请求头
    String headerValue = exchange.getRequest().getHeaders().getFirst(config.getHeaderKey());

    // 2. 判断请求头是否存在且值是否匹配配置
    boolean match = headerValue != null && headerValue.equals(config.getHeaderValue());

    if (match) {
    System.out.println("自定义请求头断言匹配成功: " + config.getHeaderKey() + "=" + config.getHeaderValue());
    } else {
    System.out.println("自定义请求头断言匹配失败: " + config.getHeaderKey() + "=" +
    (headerValue != null ? headerValue : "null") + ", 期望值: " + config.getHeaderValue());
    }

    return match;
    }

    @Override
    public String toString() {
    // 这个方法使得在日志中输出断言时更具可读性
    return String.format("Header: %s=%s", config.getHeaderKey(), config.getHeaderValue());
    }
    };
    }

    /**
    * 用于接收和绑定配置文件的静态内部配置类
    */
    @Getter
    @Setter
    public static class Config {
    private String headerKey;
    private String headerValue;
    }
    }

    现在,我们可以在 application.yml 中使用这个新的断言工厂了。注意,断言的名称是工厂类名前缀,即 CustomHeader

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    spring:
    cloud:
    gateway:
    routes:
    - id: custom_header_route
    # 目标URI,这里我们指向测试控制器
    uri: http://localhost:8080
    predicates:
    # 只有路径匹配 /gateway/custom-predicate-test 才会进入下一个断言判断
    - Path=/gateway/custom-predicate-test
    # 使用我们自定义的断言
    # 完整配置方式
    - name: CustomHeader
    args:
    headerKey: X-Client-Version
    headerValue: v2
    # 快捷配置方式 (更常用)
    # - CustomHeader=X-Client-Version, v2
  • CustomParamRoutePredicateFactory: 自定义请求参数断言工厂

    同理,我们也可以创建一个 CustomParamRoutePredicateFactory 来根据 URL 请求参数进行路由决策。这在需要根据特定查询参数(如 channel=xxx)将用户导向不同后端服务的场景中非常有用。

    代码结构与 CustomHeaderRoutePredicateFactory 非常相似,主要区别在于 test 方法中我们从 exchange.getRequest().getQueryParams()获取参数。

    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
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    package edu.software.ergoutree.cloudgateway.predicate;

    /**
    * 自定义请求参数断言工厂
    * 用于根据请求参数中的键值对判断是否匹配路由
    */
    @Component
    public class CustomParamRoutePredicateFactory extends AbstractRoutePredicateFactory<CustomParamRoutePredicateFactory.Config> {

    /**
    * 配置属性名称
    */
    public static final String PARAM_KEY = "paramKey";
    public static final String PARAM_VALUE = "paramValue";

    public CustomParamRoutePredicateFactory() {
    super(Config.class);
    }

    /**
    * 快捷配置参数顺序
    */
    @Override
    public List<String> shortcutFieldOrder() {
    return Arrays.asList(PARAM_KEY, PARAM_VALUE);
    }

    /**
    * 断言逻辑
    */
    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
    return new GatewayPredicate() {
    @Override
    public boolean test(ServerWebExchange exchange) {
    // 获取请求参数
    String paramValue = exchange.getRequest().getQueryParams().getFirst(config.getParamKey());

    // 判断请求参数是否存在且值匹配
    boolean match = paramValue != null && paramValue.equals(config.getParamValue());

    if (match) {
    System.out.println("自定义请求参数断言匹配成功: " + config.getParamKey() + "=" + config.getParamValue());
    } else {
    System.out.println("自定义请求参数断言匹配失败: " + config.getParamKey() + "=" +
    (paramValue != null ? paramValue : "null") + ", 期望值: " + config.getParamValue());
    }

    return match;
    }

    @Override
    public String toString() {
    return String.format("Param: %s=%s", config.getParamKey(), config.getParamValue());
    }
    };
    }

    /**
    * 配置类
    */
    public static class Config {
    private String paramKey;
    private String paramValue;

    public String getParamKey() {
    return paramKey;
    }

    public void setParamKey(String paramKey) {
    this.paramKey = paramKey;
    }

    public String getParamValue() {
    return paramValue;
    }

    public void setParamValue(String paramValue) {
    this.paramValue = paramValue;
    }
    }
    }

    在配置文件中配置和上面的基本相同,留给大家进行测试吧

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    spring:
    cloud:
    gateway:
    routes:
    # ... 其他路由 ...
    - id: custom_param_route
    uri: http://localhost:8080
    predicates:
    - Path=/gateway/custom-predicate-test
    # 使用自定义参数断言 (快捷方式)
    - CustomParam=source, mobile

    这条规则意味着,只有当请求路径是 /gateway/custom-predicate-test 并且 URL 中带有查询参数 ?source=mobile 时,该路由才会被匹配。

  • GatewayTestController: 测试控制器

    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
    package edu.software.ergoutree.cloudgateway.controller;

    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import reactor.core.publisher.Mono;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 网关测试控制器
    */
    @RestController
    @RequestMapping("/gateway")
    public class GatewayTestController {

    /**
    * 测试端点
    */
    @GetMapping("/test")
    public Mono<Map<String, Object>> test() {
    Map<String, Object> result = new HashMap<>();
    result.put("message", "Gateway Test Controller");
    result.put("status", "OK");
    result.put("timestamp", System.currentTimeMillis());
    return Mono.just(result);
    }

    /**
    * 测试自定义断言端点
    */
    @GetMapping("/custom-predicate-test")
    public Mono<Map<String, Object>> customPredicateTest() {
    Map<String, Object> result = new HashMap<>();
    result.put("message", "Custom Predicate Test Endpoint");
    result.put("status", "OK");
    result.put("timestamp", System.currentTimeMillis());
    return Mono.just(result);
    }
    }

我们进行自定义请求头的断言测试,使用以下请测试 CustomHeader 断言 (基于上面的配置)

1
2
3
4
5
6
# 成功测试 - 提供正确的请求头
curl -H "X-Custom-Auth: ergou123" http://localhost:8089/api/header-test/custom-predicate-test

# 失败测试 - 不提供请求头或提供错误的请求头
curl http://localhost:8089/api/header-test/custom-predicate-test
curl -H "X-Custom-Auth: wrong-value" http://localhost:8089/api/header-test/custom-predicate-test

然后测试自定义请求参数断言,别忘了将 yml 中的配置切换为 CustomParam

1
2
3
4
5
6
# 成功测试 - 提供正确的请求参数
curl "http://localhost:8089/api/param-test/custom-predicate-test?userId=123456"

# 失败测试 - 不提供请求参数或提供错误的请求参数
curl http://localhost:8089/api/param-test/custom-predicate-test
curl "http://localhost:8089/api/param-test/custom-predicate-test?userId=wrong-value"

可以看到,我们提供正确的请求头X-Custom-Auth: ergou123,和提供正确的请求参数userId=123456,都成功收到了响应,所以此时路由断言是按照我们自定义的断言工厂所设计的来的

image-20250726105001566
image-20250726105039220

自定义过滤器配置和过滤器工厂

首先,创建一个 filter 目录和自定义请求头添加过滤器工厂:

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
/**
* 自定义请求头添加过滤器工厂
* 用于向请求中添加自定义请求头
*/
@Component
public class SpAddRequestHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<SpAddRequestHeaderGatewayFilterFactory.Config> {

/**
* 配置属性名称
*/
public static final String HEADER_NAME = "headerName";
public static final String HEADER_VALUE = "headerValue";

public SpAddRequestHeaderGatewayFilterFactory() {
super(Config.class);
}

/**
* 快捷配置参数顺序
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(HEADER_NAME, HEADER_VALUE);
}

/**
* 过滤器逻辑
*/
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// 获取原始请求
ServerHttpRequest originalRequest = exchange.getRequest();

// 打印日志
System.out.println("添加请求头: " + config.getHeaderName() + "=" + config.getHeaderValue());

// 创建新的请求,添加自定义请求头
ServerHttpRequest modifiedRequest = originalRequest.mutate()
.header(config.getHeaderName(), config.getHeaderValue())
.build();

// 使用修改后的请求继续处理
return chain.filter(exchange.mutate().request(modifiedRequest).build());
};
}

/**
* 配置类
*/
public static class Config {
private String headerName;
private String headerValue;
}
}
  1. 继承抽象类:通过继承AbstractGatewayFilterFactory,我们可以利用 Spring Cloud Gateway 提供的基础设施,无需从零实现所有接口方法
  2. 配置与逻辑分离:将配置参数封装在 Config 类中,使过滤器逻辑更清晰,也便于配置管理
  3. 不可变对象处理:HTTP 请求对象是不可变的,通过mutate()方法创建修改后的副本,因为这是响应式编程的设计理念
  4. 责任链模式:通过chain.filter()将请求传递给下一个过滤器,保证了过滤器链的完整性

然后,创建一个自定义响应头添加过滤器工厂:

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
/**
* 自定义响应头添加过滤器工厂
* 用于向响应中添加自定义响应头
*/
@Component
public class SpAddResponseHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<SpAddResponseHeaderGatewayFilterFactory.Config> {

/**
* 配置属性名称
*/
public static final String HEADER_NAME = "headerName";
public static final String HEADER_VALUE = "headerValue";

public SpAddResponseHeaderGatewayFilterFactory() {
super(Config.class);
}

/**
* 快捷配置参数顺序
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(HEADER_NAME, HEADER_VALUE);
}

/**
* 过滤器逻辑
*/
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// 获取响应对象
ServerHttpResponse response = exchange.getResponse();

// 打印日志
System.out.println("添加响应头: " + config.getHeaderName() + "=" + config.getHeaderValue());

// 添加响应头
response.getHeaders().add(config.getHeaderName(), config.getHeaderValue());

// 继续处理
return chain.filter(exchange);
};
}

/**
* 配置类
*/
public static class Config {
private String headerName;
private String headerValue;
}
}
  1. 响应处理时机:与请求头修改不同,响应头可以在过滤器链开始时就添加,因为响应对象会被后续处理流程复用
  2. 代码复用:与请求头过滤器结构相似,便于开发人员理解和维护
  3. 扩展性:实际项目中可以扩展此过滤器,添加条件判断(如仅对特定状态码添加响应头)

创建自定义请求日志记录全局过滤器,来演示全局过滤器的内容,用于记录所有请求的日志信息

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
57
58
59
60
61
62
/**
* 请求日志记录全局过滤器
* 全局过滤器与普通过滤器的区别在于:它会对所有路由生效
* 实现GlobalFilter接口标识这是一个全局过滤器
* 实现Ordered接口用于指定过滤器执行顺序
*/
@Component
public class RequestLoggingGlobalFilter implements GlobalFilter, Ordered {

// 使用SLF4J日志框架,而非System.out,这是生产环境的最佳实践
private static final Logger log = LoggerFactory.getLogger(RequestLoggingGlobalFilter.class);
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

/**
* 全局过滤器的核心方法
* 与普通过滤器不同,它不需要通过配置启用,会自动对所有请求生效
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求信息
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().toString();
String method = request.getMethod().toString();
String remoteAddress = request.getRemoteAddress().getHostString();

// 记录请求开始时间和时间戳
long startTime = System.currentTimeMillis();
String requestTime = LocalDateTime.now().format(formatter);

// 记录请求开始日志
log.info("请求开始 - 路径: {}, 方法: {}, 来源: {}, 时间: {}",
path, method, remoteAddress, requestTime);

// 继续处理请求链,并在处理完成后执行后续操作
// then()方法用于注册一个操作,在当前Mono完成后执行
// Mono.fromRunnable()将一个Runnable转换为Mono
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 计算请求处理时间
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;

// 获取响应状态码
int statusCode = exchange.getResponse().getStatusCode() != null ?
exchange.getResponse().getStatusCode().value() : 0;

// 记录请求结束日志,包含处理时间和状态码
log.info("请求结束 - 路径: {}, 状态码: {}, 处理时间: {}ms",
path, statusCode, executionTime);
}));
}

/**
* 定义过滤器的优先级
* 值越小,优先级越高
* 对于日志过滤器,通常需要在其他业务过滤器之前执行(记录开始时间)
* 并在其他过滤器之后完成(记录结束时间)
*/
@Override
public int getOrder() {
return -1; // 高优先级
}
}
  1. 全局生效:实现GlobalFilter接口使过滤器对所有路由生效,无需在每个路由中单独配置
  2. 执行顺序:通过Ordered接口的getOrder()方法控制过滤器执行顺序,要不然记录不准
  3. 响应式编程:使用then(Mono.fromRunnable(...))在请求处理完成后执行日志记录,这是响应式编程中处理后置操作的标准方式

更新测试控制器,添加测试过滤器的端点,方便看到各种情况

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 测试自定义过滤器端点
*/
@GetMapping("/filter-test")
public Mono<Map<String, Object>> filterTest(@RequestHeader HttpHeaders headers) {
Map<String, Object> result = new HashMap<>();
result.put("message", "Filter Test Endpoint");
result.put("status", "OK");
result.put("timestamp", System.currentTimeMillis());
result.put("headers", headers); // 返回请求头信息,便于验证自定义请求头是否添加成功
return Mono.just(result);
}

更新 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
# 自定义过滤器测试路由 - 请求头添加
- id: request-header-filter-route
uri: http://localhost:8089
predicates:
- Path=/api/req-header-filter/**
filters:
- RewritePath=/api/req-header-filter/(?<segment>.*), /gateway/$\{segment}
- name: SpAddRequestHeader
args:
headerName: X-Custom-Request-Header
headerValue: FromGatewayFilter

# 自定义过滤器测试路由 - 响应头添加
- id: response-header-filter-route
uri: http://localhost:8089
predicates:
- Path=/api/resp-header-filter/**
filters:
- RewritePath=/api/resp-header-filter/(?<segment>.*), /gateway/$\{segment}
- name: SpAddResponseHeader
args:
headerName: X-Custom-Response-Header
headerValue: FromGatewayFilter
  1. 过滤器命名:配置中的SpAddRequestHeader对应过滤器工厂类名SpAddRequestHeaderGatewayFilterFactory(去掉了后缀GatewayFilterFactory
  2. 参数映射:过滤器后的参数按照shortcutFieldOrder()定义的顺序映射到 Config 类的属性
  3. 路由隔离:为不同过滤器创建独立的测试路由,便于单独测试和验证
  4. 路径重写:使用RewritePath过滤器调整请求路径

现在来开始进行测试,使用以下命令测试请求头添加过滤器:

1
2
# 发送请求并查看返回的请求头信息
curl http://localhost:8089/api/req-header-filter/filter-test

请求会被路由到 /gateway/filter-test,并且会添加自定义请求头 X-Custom-Request-Header: FromGatewayFilter,在返回的响应中可以看到这个请求头。

1
2
# 发送请求并查看返回的响应头信息
curl -v http://localhost:8089/api/resp-header-filter/filter-test

而测试响应头添加过滤器,请求会被路由到 /gateway/filter-test,并且响应中会包含自定义响应头 X-Custom-Response-Header: FromGatewayFilter

image-20250726111957366
image-20250726111900164

到这里实操的内容也就结束了