前言
什么是网关
在复杂的微服务架构中,一个业务功能可能由多个微服务协同完成,每个微服务都有自己独立的网络地址。如果没有网关,客户端需要直接与各个微服务进行交互,这会带来一系列问题:
- 客户端复杂性增加: 客户端需要记录每个微服务的地址,并处理复杂的调用逻辑,增加了客户端的开发和维护成本。
- 认证授权复杂: 每个微服务都需要进行独立的认证和授权,导致重复开发和管理困难
- 跨域问题: 不同域的服务之间调用会存在跨域请求的问题,处理起来较为繁琐。
- 难以实现统一的非业务功能: 像日志记录、限流、熔断、监控等通用功能难以在每个微服务中统一实现和管理。
为了解决上述问题,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整合,可以实现服务注册、服务发现和动态配置。在这种架构下:
- 各个微服务实例启动后会向Nacos注册中心注册自己的信息。
- Spring Cloud Gateway从Nacos获取所有可用的服务实例列表。
- 当客户端请求到达网关时,网关会根据配置在Nacos中的路由规则,将请求转发到相应的服务实例上
- 如果路由规则或服务实例信息发生变化,Nacos会通知网关,网关会动态更新其路由表,从而实现路由的动态更新。
搭建统一网关服务的各种知识
搭建网关并注册服务的步骤
创建新的模块,引入 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>这两个依赖是网关模块必备的
编写路由配置和 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路由断言,路由发现配置,这些是必须写的
现在我们需要把需要的服务注册到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 # 可以指定配置分组在 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
23spring:
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点击 “发布”,配置即刻生效。
到这里就基本完成统一网关的搭建和服务注册的步骤了,最后一步就是进行测试了
路由断言工厂
网关路由我们主要配置的项目包括:
- 路由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
。

如果有很多个Predicate,并且一个请求满足多个Predicate,则按照配置的顺序第一个生效。
请注意:一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发。
我只列举一些常用的
1 | spring: |
当内置的断言无法满足复杂的业务逻辑时,可以自定义断言工厂。
自定义断言工厂需要继承AbstractRoutePredicateFactory<C>
,其中C是用于接收配置参数的配置类。
1 | // 必须注册为Spring Bean |
在application.yml
中使用自定义断言,工厂的名称是类名AgeRoutePredicateFactory
去掉后缀,即Age
1 | spring: |
现在,当一个请求如http://localhost:8080/sensitive-data/info?age=25
到达网关时,Age
断言会生效,判断
25 在 [18, 60)区间内,返回true,请求被成功路由。如果
age 为 17 或 65,则断言失败,该路由不匹配。
注意,无法路由是 404 错误,不是 5xx
配置路由的过滤器
什么是路由过滤器
GatewayFilter
是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
它决定了在一个匹配的请求被转发之前和之后,应该对这个请求做什么样的处理。

路由过滤器是针对单个路由规则生效的过滤器。它与特定的路由绑定,只有当请求匹配了该路由的断言条件后,这个过滤器才会被执行。
一般在在微服务架构中,路由过滤器是实现各种横切关注点和流量控制的要点,以下是常用的路由过滤器

添加请求头 / 响应头过滤器
用于在请求发送到下游服务前添加请求头,或在响应返回给客户端前添加响应头。
- 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/1
(segment
是正则分组变量)。 - 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 | spring: |
自定义路由过滤器工厂 (GatewayFilterFactory)与自定义断言类似,当内置过滤器不满足需求时,可以自定义。
我们以创建一个记录请求处理时间的过滤器为场景
1 | import org.slf4j.Logger; |
配置自定义过滤器:在application.yml
中,工厂名称是类名去掉GatewayFilterFactory
后缀。
1 | spring: |
全局过滤器
我们可以对过滤器配置
default-filters
,这是默认过滤器,会对所有的请求都生效

但是 GlobalFilter
是专门做这个的,只不过我们上面说的默认过滤器的处理逻辑是固定的,配置哪个用哪个,而
GlobalFilter
的逻辑需要你自己写代码进行实现
全局过滤器非常适合实现系统级的通用功能。
全局过滤器需要实现GlobalFilter
和Ordered
接口,并通过@Component
注解注入到Spring容器中。实现一个简单的全局身份认证过滤器为例子
1 |
|
这个全局过滤器无需在YAML中配置,只要它被Spring容器管理,就会自动对所有路由生效。getOrder()方法非常重要,它决定了多个全局过滤器的执行顺序。
过滤器的链的执行顺序
省流


不省流
首先要明确,Gateway中的过滤器逻辑上分为两个阶段:
- “Pre” 阶段:在请求被路由到下游微服务之前执行的逻辑。这是我们最常用的阶段,可以用于身份认证、请求参数修改、日志记录、限流等。
- “Post” 阶段:在下游微服务返回响应之后,响应数据返回给客户端之前执行的逻辑。常用于修改响应头、记录响应状态和处理耗时等。
这种 Pre 和 Post 的划分并不是通过配置两个不同的过滤器来实现的,而是在同一个过滤器的代码逻辑中体现的。
1 | return (exchange, chain) -> { |
当一个请求到达Gateway时,它会经过一个由多种过滤器组成的链条。这个链条的整体顺序是固定的,可以想象成一个链条结构:
请求进入 (Request) -> Pre阶段过滤器链 -> 目标微服务 -> Post阶段过滤器链 -> 响应返回 (Response)
这个链条具体由三类过滤器构成,它们的执行优先级从高到低依次是:
- 默认过滤器 (Default Filters):由Spring Cloud Gateway框架自身提供,用于实现核心的路由转发、负载均衡等功能。这些过滤器拥有最高的优先级,我们通常不需要关心它们。
- 全局过滤器 (GlobalFilter):作用于所有路由的过滤器。
- 路由过滤器 (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 | spring: |
说一下,'[/**]'
,[ ]
是YAML语法,表示这是一个Key。/**
是路径匹配符,代表拦截所有请求,对它们统一应用下面的CORS规则。
网关异常的统一处理
https://blog.csdn.net/weixin_44863237/article/details/134728229,感谢
我们在通过gateway进行路由转发时,可能会遇到各种的问题,通过统一异常处理器,可以将不同的异常进行集中式处理和管理。在开发或者使用过程中报错时,可以更加清晰明了的发现错误提示信息,方便更快地理解和解决问题,还可以优化用户体验并降低维护成本。
自定义异常处理器方式:
可以通过实现ErrorWebExceptionHandler
接口来自定义异常处理器,对全局异常进行统一处理。
在这个处理器中,可以根据异常的类型、状态码等信息,返回不同的响应结果给前端。
1 | /** |
统一网关搭建微服务模块的实例演示
基础环境搭建
首先我们创建一个 gateway 模块,处理网关的请求
网关模块加入依赖
1 | <!-- Spring Cloud Gateway --> |
启动类上添加服务发现的注解
1 |
|
编写配置文件,先编写 bootstrap,对了这个也要加依赖,我上面忘了,自己加一下,然后在这里进行 Naocs 的相关配置
1 | # bootstrap.yml 比 application.yml 优先加载,用于配置Nacos配置中心等固定的外部化配置 |
启动模块,看看能不能发现

我们拿出之前写好的 cloud-product 模块拿出来鼓捣,你可能需要自己写一个控制器,然后按照我的方式进行一步步的配置操作
现在开始进行网关相关的配置了
首先是跨域配置,我直接允许全部了,反正是自己鼓捣玩
1 | cloud: |
按照我的 cloud-product 模块,进行如下路由配置
1 | # 路由配置 |
讲一下这些内容为什么要这么写
路由id:命名规范通常是:
{服务名}-service-route或{功能}-route
,在一个网关中,每个路由ID必须唯一目标uri:
uri
定义了请求将被转发到的目标地址,lb://
前缀表示使用负载均衡(Load Balancer),cloudProduct
是在Nacos注册中心注册的服务名称对于微服务架构,通常使用lb://服务名格式,让网关通过服务发现机制找到目标服务
服务名必须与目标服务在注册中心(如Nacos)中注册的名称完全一致(区分大小写)
断言 predicates:
Path=/product/**
表示所有以/product/开头的请求都会被该路由处理**
是通配符,表示匹配任意子路径实际开发中如何配置,根据服务的访问路径模式来配置,通常与服务的context-path
保持一致
日志配置
1 | # 日志配置 |
别的配置自己看情况加
接下来在 Nacos 中配置 网关配置的 相关内容,别找错命名空间

把你的 application.yml 复制一下进行发布
到这里我们启动项目,测试一下能不能通过网关发送请求

成功了,前置内容我们已经完成,接下来我们进行上面讲的内容的各种实操
配置路由断言及其自定义路由断言工厂
网关服务一个主要作用就是路由配置,通过不同的路由配置实现相应的功能。我们一般需要自定义的有这三种组件
- Route:路由是网关的基本构件。它由ID、目标URI、路由断言工厂(谓语动词)和过滤器集合定义。如果断言工厂判断为真,则匹配路由。
- Predicate:路由断言工厂,参照Java8的新特性Predicate。这允许开发人员匹配HTTP请求中的任何内容,比如请求头或参数。
- Filter:路由过滤器。可以在发送下游请求之前或之后修改请求和响应。
配置 Spring Cloud Gateway 自带的路由断言配置,只需要在 application 配置类中加入
1 | cloud: |
我们在这里主要是讲解自定义路由断言工厂的内容,以常用的根据请求头中键值对判断是否匹配路由,因为在微服务架构中,我们常常通过请求头来传递租户信息、客户端版本等,并希望根据这些信息将请求路由到不同的服务实例,我创建的三个类分别如下
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
72package edu.software.ergoutree.cloudgateway.predicate;
/**
* 自定义请求头断言工厂
* 用于根据请求头中的键值对判断是否匹配路由
* 类名必须以 "RoutePredicateFactory" 结尾
*/
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。
*/
public List<String> shortcutFieldOrder() {
return Arrays.asList(HEADER_KEY, HEADER_VALUE);
}
/**
* 核心断言逻辑
*/
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
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;
}
public String toString() {
// 这个方法使得在日志中输出断言时更具可读性
return String.format("Header: %s=%s", config.getHeaderKey(), config.getHeaderValue());
}
};
}
/**
* 用于接收和绑定配置文件的静态内部配置类
*/
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
18spring:
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, v2CustomParamRoutePredicateFactory
: 自定义请求参数断言工厂同理,我们也可以创建一个
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
82package edu.software.ergoutree.cloudgateway.predicate;
/**
* 自定义请求参数断言工厂
* 用于根据请求参数中的键值对判断是否匹配路由
*/
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);
}
/**
* 快捷配置参数顺序
*/
public List<String> shortcutFieldOrder() {
return Arrays.asList(PARAM_KEY, PARAM_VALUE);
}
/**
* 断言逻辑
*/
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
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;
}
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
11spring:
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
42package 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;
/**
* 网关测试控制器
*/
public class GatewayTestController {
/**
* 测试端点
*/
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);
}
/**
* 测试自定义断言端点
*/
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 | # 成功测试 - 提供正确的请求头 |
然后测试自定义请求参数断言,别忘了将 yml 中的配置切换为
CustomParam
1 | # 成功测试 - 提供正确的请求参数 |
可以看到,我们提供正确的请求头X-Custom-Auth: ergou123
,和提供正确的请求参数userId=123456
,都成功收到了响应,所以此时路由断言是按照我们自定义的断言工厂所设计的来的


自定义过滤器配置和过滤器工厂
首先,创建一个 filter 目录和自定义请求头添加过滤器工厂:
1 | /** |
- 继承抽象类:通过继承
AbstractGatewayFilterFactory
,我们可以利用 Spring Cloud Gateway 提供的基础设施,无需从零实现所有接口方法 - 配置与逻辑分离:将配置参数封装在 Config 类中,使过滤器逻辑更清晰,也便于配置管理
- 不可变对象处理:HTTP
请求对象是不可变的,通过
mutate()
方法创建修改后的副本,因为这是响应式编程的设计理念 - 责任链模式:通过
chain.filter()
将请求传递给下一个过滤器,保证了过滤器链的完整性
然后,创建一个自定义响应头添加过滤器工厂:
1 | /** |
- 响应处理时机:与请求头修改不同,响应头可以在过滤器链开始时就添加,因为响应对象会被后续处理流程复用
- 代码复用:与请求头过滤器结构相似,便于开发人员理解和维护
- 扩展性:实际项目中可以扩展此过滤器,添加条件判断(如仅对特定状态码添加响应头)
创建自定义请求日志记录全局过滤器,来演示全局过滤器的内容,用于记录所有请求的日志信息
1 | /** |
- 全局生效:实现
GlobalFilter
接口使过滤器对所有路由生效,无需在每个路由中单独配置 - 执行顺序:通过
Ordered
接口的getOrder()
方法控制过滤器执行顺序,要不然记录不准 - 响应式编程:使用
then(Mono.fromRunnable(...))
在请求处理完成后执行日志记录,这是响应式编程中处理后置操作的标准方式
更新测试控制器,添加测试过滤器的端点,方便看到各种情况
1 | /** |
更新 application.yml 配置文件,添加自定义过滤器配置
1 | # 自定义过滤器测试路由 - 请求头添加 |
- 过滤器命名:配置中的
SpAddRequestHeader
对应过滤器工厂类名SpAddRequestHeaderGatewayFilterFactory
(去掉了后缀GatewayFilterFactory
) - 参数映射:过滤器后的参数按照
shortcutFieldOrder()
定义的顺序映射到 Config 类的属性 - 路由隔离:为不同过滤器创建独立的测试路由,便于单独测试和验证
- 路径重写:使用
RewritePath
过滤器调整请求路径
现在来开始进行测试,使用以下命令测试请求头添加过滤器:
1 | # 发送请求并查看返回的请求头信息 |
请求会被路由到
/gateway/filter-test
,并且会添加自定义请求头
X-Custom-Request-Header: FromGatewayFilter
,在返回的响应中可以看到这个请求头。
1 | # 发送请求并查看返回的响应头信息 |
而测试响应头添加过滤器,请求会被路由到
/gateway/filter-test
,并且响应中会包含自定义响应头
X-Custom-Response-Header: FromGatewayFilter
。


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