Spring Cloud 学习与实践(8):Spring Cloud Gateway 统一入口、路由转发与双重跨域故障演练

文章目录

  • [Spring Cloud 学习与实践(8):Spring Cloud Gateway 统一入口、路由转发与双重跨域故障演练](#Spring Cloud 学习与实践(8):Spring Cloud Gateway 统一入口、路由转发与双重跨域故障演练)
    • [1. 本章背景:为什么微服务还需要一个统一入口](#1. 本章背景:为什么微服务还需要一个统一入口)
    • [2. Gateway 在系统中的位置](#2. Gateway 在系统中的位置)
    • [3. 本章学习目标](#3. 本章学习目标)
    • [4. 本章相关目录结构](#4. 本章相关目录结构)
  • [第一部分:理解 Gateway 核心概念](#第一部分:理解 Gateway 核心概念)
    • [5. Route、Predicate 和 Filter](#5. Route、Predicate 和 Filter)
      • [5.1 Route:完整路由规则](#5.1 Route:完整路由规则)
      • [5.2 Predicate:请求是否匹配当前路由](#5.2 Predicate:请求是否匹配当前路由)
      • [5.3 Filter:转发前后加工请求或响应](#5.3 Filter:转发前后加工请求或响应)
    • [6. `lb://` 是什么](#6. lb:// 是什么)
    • [7. Gateway 请求处理链](#7. Gateway 请求处理链)
    • [8. WebFlux、Netty 与异步非阻塞](#8. WebFlux、Netty 与异步非阻塞)
  • [第二部分:搭建 cloud-gateway](#第二部分:搭建 cloud-gateway)
    • [9. 修改 cloud-gateway/pom.xml](#9. 修改 cloud-gateway/pom.xml)
    • [10. 创建 bootstrap.yml](#10. 创建 bootstrap.yml)
    • [11. 创建网关启动类](#11. 创建网关启动类)
  • 第三部分:配置网关路由
    • [12. 在 Nacos 创建 cloud-gateway-dev.yaml](#12. 在 Nacos 创建 cloud-gateway-dev.yaml)
    • [13. 路由规划](#13. 路由规划)
    • [14. 为什么测试 URL 看起来重复](#14. 为什么测试 URL 看起来重复)
    • [15. 创建 gateway.http](#15. 创建 gateway.http)
    • [16. 启动与路由验证](#16. 启动与路由验证)
  • [第四部分:理解浏览器跨域与 OPTIONS 预检](#第四部分:理解浏览器跨域与 OPTIONS 预检)
    • [17. 什么是跨域](#17. 什么是跨域)
    • [18. CORS 是什么](#18. CORS 是什么)
    • [19. 什么是 OPTIONS 预检请求](#19. 什么是 OPTIONS 预检请求)
  • [第五部分:Gateway 全局跨域配置](#第五部分:Gateway 全局跨域配置)
    • [20. 为什么跨域应尽量由 Gateway 统一处理](#20. 为什么跨域应尽量由 Gateway 统一处理)
    • [21. cloud-gateway-dev.yaml 增加 globalcors](#21. cloud-gateway-dev.yaml 增加 globalcors)
    • [22. 为什么不建议使用 allowedOrigins: "*"](#22. 为什么不建议使用 allowedOrigins: "*")
    • [23. gateway.http 增加预检请求](#23. gateway.http 增加预检请求)
  • 第六部分:使用浏览器页面验证跨域
    • [24. 创建 cors-test.html](#24. 创建 cors-test.html)
    • [25. 启动静态页面服务器](#25. 启动静态页面服务器)
  • 第七部分:双重跨域故障演练
    • [26. 故意在下游 Controller 添加 @CrossOrigin](#26. 故意在下游 Controller 添加 @CrossOrigin)
    • [27. 故障现象](#27. 故障现象)
    • [28. 为什么会出现双重跨域](#28. 为什么会出现双重跨域)
  • 第八部分:修复双重跨域
    • [29. 第一种修复:跨域统一交给 Gateway](#29. 第一种修复:跨域统一交给 Gateway)
    • [30. 第二种修复:DedupeResponseHeader 防御性去重](#30. 第二种修复:DedupeResponseHeader 防御性去重)
    • [31. DedupeResponseHeader 是什么](#31. DedupeResponseHeader 是什么)
    • [32. 最终 cloud-gateway-dev.yaml](#32. 最终 cloud-gateway-dev.yaml)
    • [33. 最终职责边界](#33. 最终职责边界)
    • [34. 完整跨域链路](#34. 完整跨域链路)
    • [35. 本章常见问题](#35. 本章常见问题)
      • [35.1 为什么 Postman 成功,浏览器仍然可能失败](#35.1 为什么 Postman 成功,浏览器仍然可能失败)
      • [35.2 OPTIONS 是业务接口吗](#35.2 OPTIONS 是业务接口吗)
      • [35.3 为什么要开启 add-to-simple-url-handler-mapping](#35.3 为什么要开启 add-to-simple-url-handler-mapping)
      • [35.4 为什么下游不建议配置 @CrossOrigin](#35.4 为什么下游不建议配置 @CrossOrigin)
      • [35.5 DedupeResponseHeader 是否可以代替删除下游 @CrossOrigin](#35.5 DedupeResponseHeader 是否可以代替删除下游 @CrossOrigin)
      • [35.6 Gateway 是否会自动调用所有下游服务](#35.6 Gateway 是否会自动调用所有下游服务)
      • [35.7 Gateway 和 OpenFeign 有什么区别](#35.7 Gateway 和 OpenFeign 有什么区别)
      • [35.8 cloud-auth 为什么还没有接入路由](#35.8 cloud-auth 为什么还没有接入路由)
    • [36. 本章故障演练总结](#36. 本章故障演练总结)
    • [37. 本章结论](#37. 本章结论)

Spring Cloud 学习与实践(8):Spring Cloud Gateway 统一入口、路由转发与双重跨域故障演练

本章目标:搭建 cloud-gateway 网关服务,让客户端通过统一入口访问 cloud-usercloud-productcloud-order;理解 Route、Predicate、Filter、lb://、WebFlux、Netty、OPTIONS 预检请求与 CORS;最后通过"双重跨域"故障演练,掌握网关层统一跨域配置和 DedupeResponseHeader 去重方案。


1. 本章背景:为什么微服务还需要一个统一入口

前七章已经完成:

text 复制代码
第 1 章:
Maven 多模块项目骨架
cloud-common 公共模块

第 2 章:
cloud-user 用户服务
MyBatis-Plus CRUD
分页失效故障演练

第 3 章:
cloud-product 商品服务
库存扣减
JMeter 并发超卖
数据库原子条件更新

第 4 章:
cloud-order 订单服务
本地创建订单
主动暴露服务边界问题

第 5 章:
Nacos 注册中心
服务实例注册
实例上下线观察

第 6 章:
Nacos 配置中心
bootstrap.yml
DataId、Group、Namespace
@RefreshScope 动态刷新

第 7 章:
OpenFeign 服务调用
cloud-api 远程契约
Nacos 服务发现
LoadBalancer
Feign 超时
fallbackFactory
错误注册 IP
Nacos Server 停机观察

目前可以直接访问三个业务服务:

text 复制代码
http://localhost:9200/users/1
http://localhost:9300/products/1
http://localhost:9400/orders/1

这种方式在学习早期便于调试,但在真实项目中存在明显问题:

text 复制代码
前端需要维护多个服务地址
内部服务 IP 和端口直接暴露
跨域配置分散
鉴权逻辑容易重复
日志、限流、灰度、审计难以统一

因此,需要增加统一入口:

text 复制代码
cloud-gateway

客户端只需要访问:

text 复制代码
http://localhost:9000

请求先进入网关,再由网关转发到具体业务服务。


2. Gateway 在系统中的位置

接入网关后,外部请求链路变为:

text 复制代码
浏览器 / App / 第三方调用方
            ↓
      cloud-gateway:9000
            ↓
       路由规则匹配
            ↓
   LoadBalancer 选择实例
            ↓
 cloud-user / cloud-product / cloud-order

网关适合统一处理:

text 复制代码
路由转发
跨域
鉴权
日志
限流
灰度
审计
请求头处理
响应头处理

一句话总结:

text 复制代码
Gateway 是微服务系统的统一入口。

3. 本章学习目标

本章完成:

text 复制代码
1. 创建 cloud-gateway 模块
2. 接入 Spring Cloud Gateway
3. 注册 cloud-gateway 到 Nacos
4. 使用 Nacos Config 管理网关路由
5. 配置 Route、Predicate 和 Filter
6. 使用 lb:// 服务名转发
7. 使用 StripPrefix 处理路径
8. 通过 Gateway 查询用户、商品和订单
9. 通过 Gateway 创建订单
10. 验证未匹配路由返回 404
11. 配置全局 CORS
12. 理解 OPTIONS 预检请求
13. 使用浏览器页面验证跨域
14. 故意在下游 Controller 添加 @CrossOrigin
15. 复现 Access-Control-Allow-Origin 重复
16. 使用 DedupeResponseHeader 去重
17. 明确跨域职责边界

本章暂时不做:

text 复制代码
JWT 鉴权
用户身份透传
自定义 GlobalFilter
Sentinel 限流
动态路由热更新

这些将在后续章节中继续学习。


4. 本章相关目录结构

完成后目录如下:

text 复制代码
cloud-demo
├── cloud-common
├── cloud-api
├── cloud-gateway
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── com.example.cloud.gateway
│       │   │       └── CloudGatewayApplication.java
│       │   └── resources
│       │       └── bootstrap.yml
│       └── test
│           └── http
│               └── gateway.http
├── cloud-auth
├── cloud-user
├── cloud-product
└── cloud-order

网关路由配置放到 Nacos 配置中心:

text 复制代码
dev Namespace
└── DEFAULT_GROUP
    └── cloud-gateway-dev.yaml

此外,项目根目录创建:

text 复制代码
cors-test.html

用于真正模拟浏览器跨域请求。


第一部分:理解 Gateway 核心概念

5. Route、Predicate 和 Filter

Spring Cloud Gateway 最重要的三个概念:

text 复制代码
Route
Predicate
Filter

5.1 Route:完整路由规则

Route 表示一条完整转发规则。

示例:

yaml 复制代码
- id: product-route
  uri: lb://cloud-product
  predicates:
    - Path=/api/product/**
  filters:
    - StripPrefix=2

含义:

text 复制代码
路由名称:
product-route

匹配条件:
请求路径符合 /api/product/**

目标服务:
cloud-product

转发前处理:
删除前两段路径

一条 Route 通常包含:

text 复制代码
id
uri
predicates
filters

5.2 Predicate:请求是否匹配当前路由

Predicate 可以理解为:

text 复制代码
路由匹配条件

例如:

yaml 复制代码
predicates:
  - Path=/api/product/**

只有符合:

text 复制代码
/api/product/**

的请求才会进入该路由。

Predicate 不只支持路径,还可以根据:

text 复制代码
请求方法
请求头
Cookie
Host
查询参数
时间
来源 IP

进行匹配。


5.3 Filter:转发前后加工请求或响应

Filter 可以理解为:

text 复制代码
网关处理链中的加工步骤

它可以在请求转发前或响应返回后执行。

例如:

yaml 复制代码
filters:
  - StripPrefix=2

表示删除请求路径前两段。

请求:

text 复制代码
/api/product/products/1

删除:

text 复制代码
/api/product

后,下游实际收到:

text 复制代码
/products/1

6. lb:// 是什么

网关路由目标不写死:

text 复制代码
http://localhost:9300

而是写:

text 复制代码
lb://cloud-product

其中:

text 复制代码
lb:
通过 LoadBalancer 选择实例

cloud-product:
Nacos 中注册的服务名

处理链:

text 复制代码
lb://cloud-product
        ↓
LoadBalancer 查询实例列表
        ↓
例如:
192.168.1.3:9300
        ↓
选择一个实例
        ↓
转发 HTTP 请求

这和第 7 章中的:

java 复制代码
@FeignClient(name = "cloud-product")

思路一致。

区别在于:

场景 写法
Java 业务代码调用远程服务 @FeignClient(name = "cloud-product")
Gateway 配置目标服务 uri: lb://cloud-product

共同点:

text 复制代码
服务名
    ↓
服务发现
    ↓
实例列表
    ↓
IP 和端口

7. Gateway 请求处理链

请求:

text 复制代码
GET /api/product/products/1

执行过程:

text 复制代码
客户端请求
        ↓
cloud-gateway:9000
        ↓
Predicate 判断是否匹配:
/api/product/**
        ↓
匹配 product-route
        ↓
Filter 执行:
StripPrefix=2
        ↓
路径变为:
/products/1
        ↓
目标:
lb://cloud-product
        ↓
LoadBalancer 查询 Nacos 实例
        ↓
选择:
192.168.1.3:9300
        ↓
转发:
GET /products/1
        ↓
cloud-product 返回结果
        ↓
Gateway 将响应返回客户端

8. WebFlux、Netty 与异步非阻塞

cloud-usercloud-productcloud-order 当前使用:

text 复制代码
Spring MVC
Tomcat
阻塞式请求模型

cloud-gateway 使用:

text 复制代码
Spring WebFlux
Project Reactor
Netty
异步非阻塞模型

为什么网关适合异步非阻塞

网关处于系统入口,面对大量并发连接。

很多时间花在:

text 复制代码
等待下游服务响应

异步非阻塞模型不会让线程长期阻塞在等待过程中。

当前阶段不需要深入 Reactor 源码,只需理解:

text 复制代码
Gateway 适合作为高并发入口,
它基于 WebFlux 和 Netty,
不应混入传统 Servlet Web 技术栈。

特别注意

cloud-gateway 中不要引入:

xml 复制代码
spring-boot-starter-web

否则会混入 Spring MVC / Servlet 依赖,增加冲突和理解成本。


第二部分:搭建 cloud-gateway

9. 修改 cloud-gateway/pom.xml

完整内容:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.example.cloud</groupId>
        <artifactId>cloud-demo</artifactId>
        <version>1.0.0</version>
    </parent>

    <artifactId>cloud-gateway</artifactId>
    <packaging>jar</packaging>

    <dependencies>
        <!--
            Spring Cloud Gateway:
            提供 Route、Predicate、Filter 和 Netty 转发能力。

            注意:
            Gateway 基于 WebFlux,
            不要同时引入 spring-boot-starter-web。
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <!--
            Nacos Discovery:
            1. 将 cloud-gateway 注册到 Nacos
            2. 让网关发现 cloud-user、cloud-product、cloud-order
        -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--
            LoadBalancer:
            支持 lb://cloud-user 这种服务名转发方式。
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

        <!--
            Nacos Config:
            从 Nacos 配置中心读取 cloud-gateway-dev.yaml。
        -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>

        <!--
            启用 bootstrap.yml 启动阶段。
        -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
    </dependencies>

</project>

10. 创建 bootstrap.yml

位置:

text 复制代码
cloud-gateway
└── src/main/resources
    └── bootstrap.yml

内容:

yaml 复制代码
spring:
  application:
    # 服务名:
    # 1. Gateway 注册到 Nacos 时使用
    # 2. 推导 Nacos Config 的 DataId
    name: cloud-gateway

  profiles:
    active: dev

  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        group: DEFAULT_GROUP

        # 使用第 6 章创建的 dev Namespace ID。
        # 注意:填写 ID,不是显示名称 dev。
        namespace: 你的-dev-Namespace-ID

需要注意:

text 复制代码
spring.cloud.nacos.config.namespace

表示:

text 复制代码
配置中心使用哪个 Namespace

它不等于:

text 复制代码
注册中心的 Namespace

当前网关和业务服务仍然注册在默认注册命名空间中。


11. 创建网关启动类

位置:

text 复制代码
cloud-gateway
└── src/main/java
    └── com.example.cloud.gateway
        └── CloudGatewayApplication.java

代码:

java 复制代码
package com.example.cloud.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

/**
 * 网关启动类。
 *
 * 当前职责:
 * 1. 注册到 Nacos
 * 2. 接收客户端请求
 * 3. 根据 Route 和 Predicate 匹配下游服务
 * 4. 执行 Filter
 * 5. 通过 lb:// 服务名完成负载转发
 */
@EnableDiscoveryClient
@SpringBootApplication
public class CloudGatewayApplication {

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

第三部分:配置网关路由

12. 在 Nacos 创建 cloud-gateway-dev.yaml

进入:

text 复制代码
Nacos 控制台
    ↓
配置管理
    ↓
配置列表
    ↓
dev Namespace
    ↓
创建配置

填写:

配置项
Data ID cloud-gateway-dev.yaml
Group DEFAULT_GROUP
配置格式 YAML

初始配置:

yaml 复制代码
server:
  port: 9000

spring:
  cloud:
    nacos:
      discovery:
        # Gateway 自己注册到 Nacos,
        # 同时通过 Nacos 发现下游服务。
        server-addr: 127.0.0.1:8848

    gateway:
      routes:
        # 用户服务路由
        - id: user-route
          uri: lb://cloud-user
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=2

        # 商品服务路由
        - id: product-route
          uri: lb://cloud-product
          predicates:
            - Path=/api/product/**
          filters:
            - StripPrefix=2

        # 订单服务路由
        - id: order-route
          uri: lb://cloud-order
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=2

13. 路由规划

当前规划:

text 复制代码
/api/user/**    -> cloud-user
/api/product/** -> cloud-product
/api/order/**   -> cloud-order

第 9 章增加认证服务后,再补:

text 复制代码
/api/auth/**    -> cloud-auth

14. 为什么测试 URL 看起来重复

例如:

text 复制代码
/api/product/products/1

看起来有两层 product。

原因是本章先使用最直观的:

yaml 复制代码
StripPrefix=2

执行:

text 复制代码
/api/product/products/1
        ↓
删除 /api/product
        ↓
/products/1

这样可以清楚观察 Filter 行为。

后续可以用:

text 复制代码
RewritePath

将外部地址优化为:

text 复制代码
/api/product/1

再转发成:

text 复制代码
/products/1

当前先完成最小闭环。


15. 创建 gateway.http

位置:

text 复制代码
cloud-gateway
└── src/test/http
    └── gateway.http

内容:

http 复制代码
### 通过 Gateway 查询用户
GET http://localhost:9000/api/user/users/1

### 通过 Gateway 查询商品
GET http://localhost:9000/api/product/products/1

### 通过 Gateway 查询订单
# 将 1 替换为数据库中真实存在的订单 ID。
GET http://localhost:9000/api/order/orders/1

### 通过 Gateway 创建订单
POST http://localhost:9000/api/order/orders
Content-Type: application/json

{
  "userId": 1,
  "productId": 1,
  "quantity": 1
}

### 不匹配任何路由
GET http://localhost:9000/api/unknown/test

16. 启动与路由验证

启动顺序:

text 复制代码
1. Nacos Server
2. CloudUserApplication
3. CloudProductApplication
4. CloudOrderApplication
5. CloudGatewayApplication

Nacos 服务列表中应出现:

text 复制代码
cloud-user
cloud-product
cloud-order
cloud-gateway

网关端口:

text 复制代码
9000

实际验证结果:

text 复制代码
cloud-gateway 启动成功
Nacos 中出现 cloud-gateway:9000
GET /api/user/users/1 成功
GET /api/product/products/1 成功
GET /api/order/orders/{id} 成功
POST /api/order/orders 成功创建订单并扣减库存
GET /api/unknown/test 返回 404

这说明:

text 复制代码
Route
Predicate
Filter
StripPrefix
lb://
Nacos Discovery
LoadBalancer

均已生效。


第四部分:理解浏览器跨域与 OPTIONS 预检

17. 什么是跨域

浏览器页面和接口的:

text 复制代码
协议
域名
端口

只要有一项不同,就属于不同 Origin。

例如:

text 复制代码
前端页面:
http://localhost:5500

Gateway:
http://localhost:9000

端口不同,所以属于跨域。

需要注意:

text 复制代码
跨域主要是浏览器安全策略

IDEA HTTP Client、Postman 和后端代码通常不会受浏览器 CORS 限制。

因此:

text 复制代码
gateway.http 能调用成功

不代表:

text 复制代码
浏览器页面一定能成功调用

18. CORS 是什么

CORS 全称:

text 复制代码
Cross-Origin Resource Sharing
跨源资源共享

浏览器默认限制页面随意读取其他 Origin 的响应。

服务端需要通过响应头明确告诉浏览器:

text 复制代码
允许哪些来源
允许哪些方法
允许哪些请求头
是否允许 Cookie

常见响应头:

text 复制代码
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Allow-Credentials

19. 什么是 OPTIONS 预检请求

浏览器发送部分跨域请求前,会先发送:

http 复制代码
OPTIONS

询问服务端:

text 复制代码
是否允许当前来源?
是否允许 POST?
是否允许 Content-Type?
是否允许自定义请求头?

这个过程叫:

text 复制代码
Preflight Request
预检请求

例如浏览器准备发送:

http 复制代码
POST /api/order/orders
Content-Type: application/json

可能先发送:

http 复制代码
OPTIONS /api/order/orders
Origin: http://localhost:5500
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

只有预检成功,浏览器才会发送正式 POST 请求。


第五部分:Gateway 全局跨域配置

20. 为什么跨域应尽量由 Gateway 统一处理

外部请求已经统一经过 Gateway:

text 复制代码
客户端
    ↓
Gateway
    ↓
业务服务

因此跨域也适合统一配置在网关:

text 复制代码
Gateway 统一处理 CORS
业务服务只处理业务逻辑

这样可以避免:

text 复制代码
每个服务重复配置
配置规则不一致
Gateway 和下游重复添加响应头

21. cloud-gateway-dev.yaml 增加 globalcors

将 Nacos 中:

text 复制代码
dev Namespace
└── DEFAULT_GROUP
    └── cloud-gateway-dev.yaml

更新为:

yaml 复制代码
server:
  port: 9000

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

    gateway:
      # 全局跨域配置
      globalcors:
        # OPTIONS 预检请求有时无法匹配普通业务路由。
        # 开启后,预检请求即使未命中 Route,
        # 也可以由 Gateway 处理。
        add-to-simple-url-handler-mapping: true

        cors-configurations:
          '[/**]':
            # 当前前端测试页面运行在 5500 端口。
            allowedOrigins:
              - "http://localhost:5500"

            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS

            allowedHeaders:
              - "*"

            # 允许携带 Cookie 等凭据。
            allowCredentials: true

            # 浏览器缓存预检结果的秒数。
            maxAge: 3600

      routes:
        - id: user-route
          uri: lb://cloud-user
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=2

        - id: product-route
          uri: lb://cloud-product
          predicates:
            - Path=/api/product/**
          filters:
            - StripPrefix=2

        - id: order-route
          uri: lb://cloud-order
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=2

发布配置后,重新启动:

text 复制代码
CloudGatewayApplication

22. 为什么不建议使用 allowedOrigins: "*"

当前配置:

yaml 复制代码
allowCredentials: true

表示允许携带 Cookie 等凭据。

此时不建议配置:

yaml 复制代码
allowedOrigins:
  - "*"

更合适的是明确指定前端地址:

yaml 复制代码
allowedOrigins:
  - "http://localhost:5500"

正式环境中应替换为真实前端域名。


23. gateway.http 增加预检请求

http 复制代码
### 模拟浏览器 OPTIONS 预检请求
OPTIONS http://localhost:9000/api/order/orders
Origin: http://localhost:5500
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

### 带 Origin 请求头查询商品
GET http://localhost:9000/api/product/products/1
Origin: http://localhost:5500

实际结果:

text 复制代码
OPTIONS 预检请求成功
响应中包含允许跨域的响应头

例如:

text 复制代码
Access-Control-Allow-Origin: http://localhost:5500
Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS
Access-Control-Allow-Credentials: true

重点不是:

text 复制代码
200 还是 204

而是:

text 复制代码
浏览器获得允许跨域的响应头

第六部分:使用浏览器页面验证跨域

24. 创建 cors-test.html

位置:

text 复制代码
cloud-demo
└── cors-test.html

完整内容:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Gateway CORS Test</title>
</head>
<body>
<h1>Spring Cloud Gateway 跨域测试</h1>

<button id="queryProduct">通过 Gateway 查询商品</button>

<pre id="result"></pre>

<script>
    const result = document.getElementById("result");

    document
        .getElementById("queryProduct")
        .addEventListener("click", async () => {
            result.textContent = "请求中...";

            try {
                const response = await fetch(
                    "http://localhost:9000/api/product/products/1"
                );

                const data = await response.json();

                result.textContent = JSON.stringify(data, null, 2);
            } catch (error) {
                result.textContent = String(error);
                console.error(error);
            }
        });
</script>
</body>
</html>

25. 启动静态页面服务器

cors-test.html 所在目录打开终端。

Windows下 Python 3 环境优先尝试:

bat 复制代码
py -m http.server 5500

如果命令不可用,再尝试:

bat 复制代码
python -m http.server 5500

Python 2则用:

bash 复制代码
python -m SimpleHTTPServer 5500

浏览器打开:

text 复制代码
http://localhost:5500/cors-test.html

点击:

text 复制代码
通过 Gateway 查询商品

实际结果:

text 复制代码
浏览器页面成功显示商品 JSON

这说明:

text 复制代码
Gateway 全局 CORS 已生效

第七部分:双重跨域故障演练

26. 故意在下游 Controller 添加 @CrossOrigin

Gateway 已经配置全局 CORS。

为了复现双重跨域问题,在商品服务中临时添加:

text 复制代码
cloud-product
└── src/main/java
    └── com.example.cloud.product.controller
        └── ProductController.java

新增导入:

java 复制代码
import org.springframework.web.bind.annotation.CrossOrigin;

在类上添加:

java 复制代码
/**
 * 仅用于双重跨域故障演练。
 *
 * Gateway 已经配置全局 CORS。
 * 当前在下游 Controller 再次添加 @CrossOrigin,
 * 可能导致响应头重复。
 *
 * 演练结束后必须删除。
 */
@CrossOrigin(origins = "http://localhost:5500")
@RestController
@RequestMapping("/products")
@RequiredArgsConstructor
public class ProductController {
    // 原有代码保持不变
}

重新启动:

text 复制代码
CloudProductApplication

27. 故障现象

再次打开:

text 复制代码
http://localhost:5500/cors-test.html

点击查询商品。

浏览器出现跨域报错。

F12 查看:

text 复制代码
Network
    ↓
products/1
    ↓
Response Headers

发现:

text 复制代码
Access-Control-Allow-Origin

重复。

可能显示为:

text 复制代码
Access-Control-Allow-Origin: http://localhost:5500
Access-Control-Allow-Origin: http://localhost:5500

也可能合并为:

text 复制代码
Access-Control-Allow-Origin:
http://localhost:5500, http://localhost:5500

实际演练结果:

text 复制代码
响应头出现重复
浏览器跨域报错

28. 为什么会出现双重跨域

请求经过 Gateway:

text 复制代码
Gateway globalcors
        ↓
添加 Access-Control-Allow-Origin

然后进入商品服务:

text 复制代码
ProductController @CrossOrigin
        ↓
再次添加 Access-Control-Allow-Origin

响应返回时:

text 复制代码
Gateway 跨域响应头
+
下游跨域响应头

最终重复。

这就是:

text 复制代码
双重跨域

第八部分:修复双重跨域

29. 第一种修复:跨域统一交给 Gateway

删除商品服务中的:

java 复制代码
@CrossOrigin(origins = "http://localhost:5500")

重新启动:

text 复制代码
CloudProductApplication

再次访问浏览器测试页面。

实际结果:

text 复制代码
跨域恢复正常

推荐原则:

text 复制代码
外部请求统一经过 Gateway
        ↓
跨域统一由 Gateway 处理
        ↓
下游业务服务不重复配置 @CrossOrigin

30. 第二种修复:DedupeResponseHeader 防御性去重

真实项目中,可能存在:

text 复制代码
历史服务
第三方服务
多人协作
遗留 @CrossOrigin

为了避免响应头重复,可以在 Gateway 增加防御性去重。

在:

text 复制代码
cloud-gateway-dev.yaml

中增加:

yaml 复制代码
spring:
  cloud:
    gateway:
      # 防御性处理:
      # Gateway 和下游如果同时添加跨域响应头,
      # 只保留第一个值。
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_FIRST

31. DedupeResponseHeader 是什么

DedupeResponseHeader 是 Gateway Filter。

它用于:

text 复制代码
删除重复响应头

语法:

yaml 复制代码
- DedupeResponseHeader=响应头列表, 策略

当前配置:

yaml 复制代码
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_FIRST

含义:

text 复制代码
针对:
Access-Control-Allow-Credentials
Access-Control-Allow-Origin

如果存在重复值:
只保留第一个

常见策略:

策略 含义
RETAIN_FIRST 保留第一个值
RETAIN_LAST 保留最后一个值
RETAIN_UNIQUE 保留唯一值

本章使用:

text 复制代码
RETAIN_FIRST

32. 最终 cloud-gateway-dev.yaml

推荐最终配置:

yaml 复制代码
server:
  port: 9000

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

    gateway:
      # 全局跨域
      globalcors:
        add-to-simple-url-handler-mapping: true

        cors-configurations:
          '[/**]':
            allowedOrigins:
              - "http://localhost:5500"

            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS

            allowedHeaders:
              - "*"

            allowCredentials: true
            maxAge: 3600

      # 防御性响应头去重
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin, RETAIN_FIRST

      routes:
        - id: user-route
          uri: lb://cloud-user
          predicates:
            - Path=/api/user/**
          filters:
            - StripPrefix=2

        - id: product-route
          uri: lb://cloud-product
          predicates:
            - Path=/api/product/**
          filters:
            - StripPrefix=2

        - id: order-route
          uri: lb://cloud-order
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=2

33. 最终职责边界

推荐最终状态:

text 复制代码
Gateway:
保留 globalcors
保留 DedupeResponseHeader

cloud-user:
不配置 @CrossOrigin

cloud-product:
删除临时 @CrossOrigin

cloud-order:
不配置 @CrossOrigin

一句话总结:

text 复制代码
跨域统一放在网关;
下游服务只处理业务;
Gateway 使用 DedupeResponseHeader 做防御性去重。

34. 完整跨域链路

正常链路:

text 复制代码
浏览器页面:
http://localhost:5500
        ↓
请求 Gateway:
http://localhost:9000
        ↓
浏览器识别为跨域
        ↓
复杂请求先发送 OPTIONS
        ↓
Gateway globalcors 处理预检
        ↓
浏览器发送正式请求
        ↓
Gateway 匹配 Route
        ↓
执行 Filter
        ↓
转发下游服务
        ↓
Gateway 返回跨域响应头
        ↓
浏览器允许 JavaScript 读取响应

故障链:

text 复制代码
Gateway 配置 globalcors
        ↓
下游 Controller 又配置 @CrossOrigin
        ↓
Access-Control-Allow-Origin 重复
        ↓
浏览器拒绝响应

修复链:

text 复制代码
删除下游 @CrossOrigin
        ↓
跨域职责集中到 Gateway
        ↓
Gateway 增加 DedupeResponseHeader
        ↓
防御性去重

35. 本章常见问题

35.1 为什么 Postman 成功,浏览器仍然可能失败

因为:

text 复制代码
CORS 是浏览器安全策略

Postman 和后端调用通常不执行浏览器同源限制。


35.2 OPTIONS 是业务接口吗

不是。

它是浏览器为了确认跨域权限发送的:

text 复制代码
预检请求

35.3 为什么要开启 add-to-simple-url-handler-mapping

OPTIONS 请求可能无法匹配普通业务 Route。

开启后,即使预检请求未命中 Route,也可以由 Gateway 的全局 CORS 逻辑处理。


35.4 为什么下游不建议配置 @CrossOrigin

如果所有外部请求都经过 Gateway:

text 复制代码
跨域应统一由 Gateway 管理

下游重复配置容易导致:

text 复制代码
响应头重复
规则不一致
排查困难

35.5 DedupeResponseHeader 是否可以代替删除下游 @CrossOrigin

不能完全代替。

推荐优先:

text 复制代码
删除重复职责

同时保留:

text 复制代码
DedupeResponseHeader

作为防御性配置。


35.6 Gateway 是否会自动调用所有下游服务

不会。

Gateway 只根据:

text 复制代码
Route
Predicate
Filter

转发当前请求。


35.7 Gateway 和 OpenFeign 有什么区别

场景 组件
外部客户端进入系统 Gateway
服务内部调用其他服务 OpenFeign

共同点:

text 复制代码
都可以基于服务名发现实例

35.8 cloud-auth 为什么还没有接入路由

cloud-auth 当前仍是骨架。

第 9 章实现登录和 JWT 后,再增加:

text 复制代码
/api/auth/** -> cloud-auth

36. 本章故障演练总结

路由转发

text 复制代码
Gateway 启动
        ↓
注册到 Nacos
        ↓
配置三条 Route
        ↓
通过 lb:// 服务名发现实例
        ↓
StripPrefix 处理路径
        ↓
成功访问用户、商品和订单

未匹配路由

text 复制代码
GET /api/unknown/test
        ↓
无 Route 匹配
        ↓
返回 404

全局 CORS

text 复制代码
Gateway 配置 globalcors
        ↓
OPTIONS 预检成功
        ↓
浏览器页面成功读取商品接口

双重跨域

text 复制代码
Gateway 已配置 globalcors
        ↓
ProductController 又添加 @CrossOrigin
        ↓
Access-Control-Allow-Origin 重复
        ↓
浏览器跨域报错

修复

text 复制代码
删除下游 @CrossOrigin
        ↓
跨域恢复正常
        ↓
Gateway 增加 DedupeResponseHeader
        ↓
防止重复响应头

37. 本章结论

本章完成:

text 复制代码
cloud-gateway
Spring Cloud Gateway
WebFlux
Netty
Route
Predicate
Filter
StripPrefix
lb://
Nacos Discovery
LoadBalancer
统一入口
路由转发
404 路由验证
CORS
Origin
OPTIONS
Preflight Request
globalcors
add-to-simple-url-handler-mapping
@CrossOrigin
双重跨域
Access-Control-Allow-Origin 重复
DedupeResponseHeader
RETAIN_FIRST

当前项目已经具备:

text 复制代码
服务注册
配置中心
环境隔离
OpenFeign 跨服务调用
远程调用降级
统一网关入口
服务名路由转发
全局跨域
响应头去重

下一章进入:

text 复制代码
第 9 章:Gateway + JWT 统一鉴权

下一章会完成:

text 复制代码
cloud-auth 登录认证
JWT 签发
Gateway 白名单
GlobalFilter
Authorization
Token 校验
X-User-Id 请求头透传
下游用户上下文
ThreadLocal
异步线程身份丢失故障演练
Feign RequestInterceptor
相关推荐
初圣魔门首席弟子1 小时前
学习工作方法论与任务执行计划
学习
夕除1 小时前
Spring Security 配置类(SecurityConfig)
java·后端·spring
skywalk81632 小时前
记录段言的开发过程
开发语言·学习·编程
知识分享小能手2 小时前
Hadoop学习教程,从入门到精通, MapReduce分布式计算框架 — 完整知识点与代码案例(4)
hadoop·学习·mapreduce
摇滚侠2 小时前
SpringMVC 入门到实战 简介和入门案例 01-13
java·后端·spring·intellij-idea
YM52e2 小时前
鸿蒙HarmonyOS ArkTS 实战:教师座椅出入记录 APP 从零到一
学习·华为·harmonyos·鸿蒙系统
云烟成雨TD2 小时前
Spring AI Alibaba 1.x 系列【73】两步 RAG
java·人工智能·spring
轮子飞了2 小时前
Spring Ai 集成 DashScope 多模态模型实现身份证信息识别
java·人工智能·spring
踏着七彩祥云的小丑2 小时前
嵌入式测试第 32 天:升级测试:固件OTA升级、断点续传、回滚测试
单片机·嵌入式硬件·学习