文章目录
-
- 一、开篇:过滤器是网关的"执行引擎"
- 二、过滤器体系与执行顺序
-
- [2.1 两种过滤器的本质区别](#2.1 两种过滤器的本质区别)
- [2.2 过滤器链的合并与排序](#2.2 过滤器链的合并与排序)
- [2.3 Pre和Post两个阶段](#2.3 Pre和Post两个阶段)
- 三、局部过滤器实战
-
- [3.1 常用内置局部过滤器](#3.1 常用内置局部过滤器)
- [3.2 StripPrefix:去除路径前缀](#3.2 StripPrefix:去除路径前缀)
- [3.3 AddRequestHeader:添加内部调用Header](#3.3 AddRequestHeader:添加内部调用Header)
- [3.4 局部过滤器与全局过滤器的选用](#3.4 局部过滤器与全局过滤器的选用)
- 四、全局过滤器实战
-
- [4.1 实现GlobalFilter接口](#4.1 实现GlobalFilter接口)
- [4.2 全局鉴权过滤器](#4.2 全局鉴权过滤器)
- [4.3 响应结果统一封装](#4.3 响应结果统一封装)
- [4.4 全局异常拦截](#4.4 全局异常拦截)
- [4.5 请求耗时统计过滤器](#4.5 请求耗时统计过滤器)
- 五、跨域统一配置
-
- [5.1 CORS跨域问题的本质](#5.1 CORS跨域问题的本质)
- [5.2 YAML方式配置CORS](#5.2 YAML方式配置CORS)
- [5.3 Java配置方式](#5.3 Java配置方式)
- [5.4 生产环境CORS安全规范](#5.4 生产环境CORS安全规范)
- [5.5 跨域配置的常见坑](#5.5 跨域配置的常见坑)
- 六、过滤器开发最佳实践
-
- [6.1 过滤器顺序设计原则](#6.1 过滤器顺序设计原则)
- [6.2 过滤器中的线程安全问题](#6.2 过滤器中的线程安全问题)
- [6.3 过滤器中的异常处理](#6.3 过滤器中的异常处理)
- 七、踩坑指南
- 八、课后作业
- 九、下节预告
- [🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航](#🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航)

适配版本 :Spring Cloud Gateway 5.0.0、Spring Cloud 2025.1.3(Oakwood)、Spring Boot 4.0.8、Spring Cloud Alibaba 2025.1.0.0、JDK 21
课程定位:网关核心能力进阶,从局部过滤器到全局过滤器,从跨域配置到统一异常处理,构建网关层的完整拦截体系
一、开篇:过滤器是网关的"执行引擎"
第18课我们完成了路由规则的深度实战。路由解决了"请求去哪里"的问题,但请求在经过网关时,往往还需要做很多事情:验证Token是否合法、记录访问日志、添加内部调用Header、统一包装响应格式、处理跨域请求、捕获并友好展示异常。
这些跨切面关注点 ,如果分散在每个后端服务中实现,会导致大量重复代码,且难以统一维护。Gateway的过滤器(Filter) 机制,正是为了在网关层统一解决这些问题而设计的。
Gateway的过滤器体系分为两类:局部过滤器(GatewayFilter) 只对特定路由生效,全局过滤器(GlobalFilter) 对所有路由生效。两者在执行时合并为一条链,按order值排序后依次执行。这个设计与Servlet的Filter链非常相似,但基于Reactor的异步模型实现,理解其执行顺序和线程模型是掌握Gateway过滤器的关键。
本课将从过滤器的分类与执行顺序讲起,深入Pre和Post两个阶段的处理逻辑,实战跨域配置、参数校验、响应统一封装、异常统一拦截和全局日志打印,最后总结过滤器开发的最佳实践。
二、过滤器体系与执行顺序
2.1 两种过滤器的本质区别
Gateway的过滤器分为两类,核心区别在于作用范围和注册方式:
| 维度 | GatewayFilter(局部过滤器) | GlobalFilter(全局过滤器) |
|---|---|---|
| 作用范围 | 仅对配置了该过滤器的路由生效 | 对所有路由生效 |
| 注册方式 | 在路由的filters列表中声明 |
实现GlobalFilter接口并注册为Bean |
| 使用场景 | 特定路由的Header添加、路径重写 | 鉴权、日志、限流、异常处理 |
| 配置灵活性 | 高(可针对每个路由不同) | 低(全局统一行为) |
| 典型内置实现 | AddRequestHeaderGatewayFilter |
GatewayMetricsFilter |
核心理解:局部过滤器是"路由级别"的,全局过滤器是"应用级别"的。鉴权、日志、异常处理这类所有路由都需要的逻辑,应该用全局过滤器;特定路由的参数修改、路径重写,应该用局部过滤器。
2.2 过滤器链的合并与排序
当一个请求匹配到某个路由后,Gateway会将该路由的局部过滤器 和所有全局过滤器 合并为一条链,按order值从小到大排序后依次执行。
请求进入
│
▼
合并过滤器链:
全局过滤器1 (order = -100)
全局过滤器2 (order = -10)
局部过滤器1 (order = 0)
全局过滤器3 (order = 1)
局部过滤器2 (order = 2)
│
▼
按 order 升序依次执行
排序规则:
order值越小,优先级越高,越先执行- 全局过滤器实现
Ordered接口或使用@Order注解指定顺序 - 局部过滤器的
order由过滤器工厂内部的OrderedGatewayFilter包装决定 - 局部过滤器和全局过滤器混合排序,而非先执行完所有全局再执行局部
典型内置过滤器的order值:
| 过滤器 | order | 职责 |
|---|---|---|
RemoveCachedBodyFilter |
HIGHEST_PRECEDENCE |
清理请求体缓存 |
AdaptCachedBodyGlobalFilter |
HIGHEST_PRECEDENCE + 1000 |
缓存请求体 |
NettyWriteResponseFilter |
-1 |
写响应回客户端 |
ForwardPathFilter |
0 |
路径转发 |
RouteToRequestUrlFilter |
10000 |
将路由URI转为请求URL |
LoadBalancerClientFilter |
10100 |
负载均衡选择实例 |
NettyRoutingFilter |
Integer.MAX_VALUE |
发起下游请求 |
关键理解 :NettyRoutingFilter的order是Integer.MAX_VALUE,意味着它是最后一个执行的Pre过滤器。它之后的所有过滤器都是Post阶段------即下游响应返回后执行的逻辑。
2.3 Pre和Post两个阶段
Gateway的过滤器执行分为两个阶段:
Pre阶段(请求转发前) :过滤器链从第一个过滤器开始,依次执行filter()方法。每个过滤器可以选择:
- 直接返回(短路),不再继续
- 调用
chain.filter(exchange),将请求传递给下一个过滤器 - 在
chain.filter(exchange)前后添加逻辑
Post阶段(响应返回后) :当NettyRoutingFilter发起下游请求并收到响应后,控制权反向回传 ,之前chain.filter()调用点之后的代码开始执行。
代码示例:
java
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// ===== Pre 阶段 =====
log.info("请求进入: {}", exchange.getRequest().getPath());
// 传递给下一个过滤器
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// ===== Post 阶段 =====
log.info("响应返回: {}", exchange.getResponse().getStatusCode());
}));
}
关键理解 :chain.filter(exchange)返回的是一个Mono<Void>,.then()之后的逻辑在响应完成后执行。这就是Post阶段的实现方式。
三、局部过滤器实战
3.1 常用内置局部过滤器
Gateway内置了大量局部过滤器工厂,日常开发中最常用的包括:
| 过滤器 | 作用 | 配置示例 |
|---|---|---|
AddRequestHeader |
添加请求头 | AddRequestHeader=X-Request-Id, 12345 |
AddRequestParameter |
添加请求参数 | AddRequestParameter=source, gateway |
AddResponseHeader |
添加响应头 | AddResponseHeader=X-Response-Time, 100ms |
RemoveRequestHeader |
删除请求头 | RemoveRequestHeader=Cookie |
RewritePath |
路径重写 | RewritePath=/api/(?<segment>.*), /${segment} |
PrefixPath |
路径前缀 | PrefixPath=/api |
StripPrefix |
去除路径前缀 | StripPrefix=1 |
Retry |
自动重试 | Retry=3 |
RequestRateLimiter |
限流 | RequestRateLimiter=... |
3.2 StripPrefix:去除路径前缀
StripPrefix用于去除请求路径的前N段。例如前端调用/api/user/1,后端服务实际接收/user/1:
yaml
spring:
cloud:
gateway:
server:
webflux:
routes:
- id: user_route
uri: lb://service-user
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1 # 去除第1段路径(/api)
StripPrefix=1表示去除路径的第1段,/api/user/1变为/user/1。
对比RewritePath :StripPrefix是按段数去除,RewritePath是按正则替换。前者更简单,后者更灵活。
3.3 AddRequestHeader:添加内部调用Header
在微服务架构中,网关是唯一的外部入口。为了安全,后端服务通常只信任来自网关的请求。网关可以在转发前添加一个内部标识Header:
yaml
filters:
- AddRequestHeader=X-Gateway-Source, internal
后端服务通过校验X-Gateway-Source: internal,拒绝绕过网关的直接访问。
生产实践 :这个Header的值应该是动态签名而非固定字符串,防止被伪造。签名算法可以基于时间戳和密钥生成,后端服务验证签名有效性。
3.4 局部过滤器与全局过滤器的选用
| 场景 | 推荐类型 | 原因 |
|---|---|---|
| 路径重写、前缀去除 | 局部过滤器 | 每个路由的路径规则不同 |
| 添加内部调用Header | 局部过滤器 | 不同服务需要的Header不同 |
| 鉴权、日志、异常处理 | 全局过滤器 | 所有路由都需要 |
| CORS跨域 | 全局配置(CorsWebFilter) |
全局统一配置 |
四、全局过滤器实战
4.1 实现GlobalFilter接口
全局过滤器通过实现GlobalFilter和Ordered接口来定义:
java
@Component
@Slf4j
public class AccessLogGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// ===== Pre 阶段 =====
long startTime = System.currentTimeMillis();
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
String method = request.getMethod().name();
String clientIp = getClientIp(request);
log.info("[网关请求] {} {} from {}", method, path, clientIp);
// ===== 传递给下一个过滤器 =====
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// ===== Post 阶段 =====
long cost = System.currentTimeMillis() - startTime;
ServerHttpResponse response = exchange.getResponse();
log.info("[网关响应] {} {} status={} cost={}ms",
method, path, response.getStatusCode(), cost);
}));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 100;
}
private String getClientIp(ServerHttpRequest request) {
// 优先从代理Header中获取真实IP
String ip = request.getHeaders().getFirst("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getHeaders().getFirst("X-Real-IP");
}
if (ip == null || ip.isEmpty()) {
InetSocketAddress remoteAddress = request.getRemoteAddress();
ip = remoteAddress != null ? remoteAddress.getAddress().getHostAddress() : "unknown";
}
return ip;
}
}
关键规范:
getOrder()返回值决定过滤器在链中的位置- 日志类过滤器应尽早执行(order值小),确保能记录完整耗时
- 响应状态的获取必须在Post阶段(
.then()之后)
4.2 全局鉴权过滤器
鉴权是网关最核心的全局过滤器。以JWT鉴权为例:
java
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
private JwtUtils jwtUtils;
private static final List<String> WHITE_LIST = List.of(
"/api/auth/login",
"/api/auth/register",
"/api/public/**"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
// 1. 白名单直接放行
if (isWhiteList(path)) {
return chain.filter(exchange);
}
// 2. 获取Token
String token = request.getHeaders().getFirst("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
return unauthorized(exchange, "缺少Token");
}
// 3. 验证Token
token = token.substring(7);
if (!jwtUtils.validate(token)) {
return unauthorized(exchange, "Token无效或已过期");
}
// 4. 解析用户信息,注入到请求头中传递给下游服务
Long userId = jwtUtils.getUserId(token);
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", String.valueOf(userId))
.build();
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
private boolean isWhiteList(String path) {
return WHITE_LIST.stream().anyMatch(pattern ->
new AntPathMatcher().match(pattern, path));
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result = Result.fail(401, message);
byte[] bytes = JSON.toJSONBytes(result);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 50; // 鉴权应尽早执行
}
}
关键设计要点:
- 鉴权过滤器的order应小于日志过滤器,确保未鉴权的请求不会被记录为正常访问
- 解析出的用户信息通过修改请求头的方式传递给下游,下游服务无需重复解析JWT
- 未授权响应必须显式设置状态码和响应体,否则返回空响应
4.3 响应结果统一封装
在网关层对响应进行统一包装,可以保证所有接口的响应格式一致:
java
@Component
@Slf4j
public class ResponseWrapperGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse originalResponse = exchange.getResponse();
// 使用装饰器模式包装响应,拦截响应体写入
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
// 仅对200响应且Content-Type为json的响应进行包装
if (body instanceof Flux && getStatusCode() == HttpStatus.OK) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 合并所有DataBuffer
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
byte[] content = new byte[dataBuffers.stream()
.mapToInt(DataBuffer::readableByteCount).sum()];
int offset = 0;
for (DataBuffer buffer : dataBuffers) {
int len = buffer.readableByteCount();
buffer.read(content, offset, len);
offset += len;
DataBufferUtils.release(buffer);
}
// 包装为统一格式
String originalBody = new String(content, StandardCharsets.UTF_8);
// 此处根据业务需求决定是否包装
return bufferFactory.wrap(content);
}));
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100; // 尽量靠后
}
}
踩坑提示 :不建议在网关层做响应体包装 。网关层包装响应需要读取并修改响应流,增加了内存开销和延迟,且容易与下游服务的统一响应格式冲突。推荐的做法是:下游服务各自使用统一的
Result格式 ,网关层只做透传。如果需要添加响应头(如链路追踪ID),使用AddResponseHeader即可,无需修改响应体。
4.4 全局异常拦截
网关层的异常分为两类:请求匹配前的异常 (如路由未找到)和下游调用异常(如连接超时)。
java
@Component
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
Result<Void> result;
HttpStatus status;
if (ex instanceof ResponseStatusException) {
ResponseStatusException rse = (ResponseStatusException) ex;
status = HttpStatus.valueOf(rse.getStatusCode().value());
if (status == HttpStatus.NOT_FOUND) {
result = Result.fail(404, "请求路径不存在: " + exchange.getRequest().getPath());
} else {
result = Result.fail(status.value(), rse.getReason());
}
} else if (ex instanceof ConnectException) {
status = HttpStatus.SERVICE_UNAVAILABLE;
result = Result.fail(503, "下游服务不可用");
} else if (ex instanceof TimeoutException) {
status = HttpStatus.GATEWAY_TIMEOUT;
result = Result.fail(504, "下游服务响应超时");
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
result = Result.fail(500, "网关内部错误");
log.error("网关未知异常", ex);
}
response.setStatusCode(status);
byte[] bytes = JSON.toJSONBytes(result);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
}
关键规范:
- 必须检查
response.isCommitted(),避免在响应已提交后再次写入 - 不同异常类型返回不同状态码:404(路由未找到)、503(下游不可用)、504(下游超时)
- 必须记录
log.error日志,便于问题排查
4.5 请求耗时统计过滤器
java
@Component
@Slf4j
public class CostTimeGlobalFilter implements GlobalFilter, Ordered {
private static final String START_TIME = "startTime";
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
Long startTime = exchange.getAttribute(START_TIME);
if (startTime != null) {
long cost = System.currentTimeMillis() - startTime;
String path = exchange.getRequest().getPath().value();
// 添加响应头,方便前端排查
exchange.getResponse().getHeaders().add("X-Response-Time", cost + "ms");
// 慢请求告警
if (cost > 1000) {
log.warn("[慢请求] {} 耗时 {}ms", path, cost);
}
}
}));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
五、跨域统一配置
5.1 CORS跨域问题的本质
浏览器出于安全考虑,默认禁止跨域请求。当页面域名(如http://localhost:3000)与API域名(如http://localhost:8080)不一致时,浏览器会先发送一个OPTIONS预检请求,服务器必须返回正确的CORS响应头,浏览器才会发送真实请求。
在微服务架构中,如果每个后端服务都配置CORS,会导致重复配置且容易遗漏。在网关层统一配置CORS是最佳实践。
5.2 YAML方式配置CORS
yaml
spring:
cloud:
gateway:
server:
webflux:
globalcors:
cors-configurations:
'[/**]': # 所有路径
allowed-origin-patterns: "*" # 允许的来源(生产环境应指定具体域名)
allowed-methods: "*" # 允许的方法
allowed-headers: "*" # 允许的请求头
allow-credentials: true # 允许携带凭证
max-age: 3600 # 预检请求缓存时间(秒)
exposed-headers: # 暴露给前端的响应头
- X-Response-Time
- X-Trace-Id
5.3 Java配置方式
java
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
5.4 生产环境CORS安全规范
| 配置项 | 开发环境 | 生产环境 |
|---|---|---|
allowed-origin-patterns |
* |
指定具体域名(如https://app.example.com) |
allowed-methods |
* |
明确列出允许的方法 |
allow-credentials |
true |
仅在需要时开启 |
max-age |
3600 | 3600 |
安全警示 :allow-credentials: true与allowed-origin-patterns: "*"同时使用时,Spring会拒绝配置(因为浏览器不允许Access-Control-Allow-Origin: *与Access-Control-Allow-Credentials: true同时存在)。生产环境应指定具体的Origin。
5.5 跨域配置的常见坑
现象 :跨域配置后,仍然报CORS policy错误。
原因排查:
- 后端服务也配置了CORS,导致响应头重复。浏览器检测到重复的
Access-Control-Allow-Origin头会拒绝。 - 预检请求
OPTIONS被鉴权过滤器拦截(返回401),浏览器认为预检失败。
解决方案:
- 网关配置CORS后,后端服务移除CORS配置
- 鉴权过滤器必须放行OPTIONS请求:
java
if (request.getMethod() == HttpMethod.OPTIONS) {
return chain.filter(exchange);
}
六、过滤器开发最佳实践
6.1 过滤器顺序设计原则
| 设计原则 | 说明 | 推荐order |
|---|---|---|
| 日志最先 | 记录请求进入时间,确保完整耗时统计 | HIGHEST_PRECEDENCE |
| 跨域次之 | CORS预检请求需要最先处理,避免被鉴权拦截 | HIGHEST_PRECEDENCE + 1 |
| 鉴权靠前 | 尽早拒绝非法请求,减少无效调用 | HIGHEST_PRECEDENCE + 50 |
| 业务处理居中 | 参数校验、Header注入 | 0 ~ 100 |
| 响应处理靠后 | 添加响应头、耗时统计 | LOWEST_PRECEDENCE - 100 |
6.2 过滤器中的线程安全问题
Gateway的过滤器会被并发调用,必须确保线程安全:
java
// ❌ 错误:使用实例变量存储请求状态
private long startTime; // 多个请求并发时会被覆盖
// ✅ 正确:使用 exchange.getAttributes() 存储请求级状态
exchange.getAttributes().put("startTime", System.currentTimeMillis());
ServerWebExchange的attributes是请求级别的,每个请求有独立的Map,是存储请求状态的标准方式。
6.3 过滤器中的异常处理
不要吞掉异常 。过滤器中的异常应该向上抛出,由ErrorWebExceptionHandler统一处理:
java
// ❌ 错误:吞掉异常,返回空响应
try {
// ...
} catch (Exception e) {
log.error("异常", e);
return Mono.empty();
}
// ✅ 正确:抛出异常,由全局异常处理器处理
// 或显式写入错误响应
七、踩坑指南
坑一:OPTIONS预检请求被鉴权拦截
现象 :跨域请求报CORS policy错误,但服务端日志显示OPTIONS请求返回401。
原因 :鉴权过滤器未放行OPTIONS请求。
解决 :在鉴权过滤器中,如果请求方法是OPTIONS,直接放行:if (HttpMethod.OPTIONS.equals(request.getMethod())) return chain.filter(exchange);
坑二:响应头添加失败
现象:在Post阶段添加响应头时,响应头未生效。
原因 :响应已提交(response.isCommitted()返回true),无法再添加响应头。
解决 :在Pre阶段添加响应头(此时响应未提交),或在NettyWriteResponseFilter之前添加。
坑三:全局过滤器未生效
现象 :实现了GlobalFilter但日志中没有输出。
原因:类未被Spring扫描到,或未注册为Bean。
解决 :确保类上有@Component注解,且位于@ComponentScan扫描路径下。
坑四:过滤器顺序与预期不符
现象:鉴权过滤器在日志过滤器之后执行,导致未鉴权请求也被记录了日志。
原因 :getOrder()返回值配置错误,或未实现Ordered接口。
解决 :显式实现Ordered接口,鉴权过滤器的order应小于日志过滤器。
坑五:修改请求体后下游接收为空
现象:在过滤器中读取了请求体,下游服务接收到的请求体为空。
原因:请求体是流式数据,被读取一次后无法再次读取。
解决 :使用ServerWebExchangeUtils.cacheRequestBody()缓存请求体,或使用AdaptCachedBodyGlobalFilter(Gateway内置,order为HIGHEST_PRECEDENCE + 1000,自动缓存请求体)。
八、课后作业
作业一 :实现一个AccessLogGlobalFilter,记录每个请求的路径、方法、客户端IP、响应状态码和耗时。验证日志输出完整。
作业二 :实现一个AuthGlobalFilter,对/api/**路径进行JWT鉴权,白名单包含/api/auth/**。未携带Token的请求返回401,鉴权通过后将X-User-Id注入请求头传递给下游服务。特别注意放行OPTIONS预检请求。
作业三 :配置网关的CORS跨域,允许http://localhost:3000的请求访问所有/api/**路径。使用浏览器或curl -H "Origin: http://localhost:3000"验证CORS响应头。
作业四(进阶) :实现一个GlobalExceptionHandler,处理路由未找到(404)、下游连接失败(503)、下游超时(504)三类异常,返回统一的Result格式。
九、下节预告
第20课将进入Gateway动态网关、集群高可用 & 生产安全方案。内容包括Nacos动态路由配置(无需重启更新路由)、网关集群部署、负载均衡、防刷限流基础、接口安全校验和网关生产最佳实践。本课完成了网关过滤器的深度实战,第20课将解决网关自身的生产可用性和安全性问题------如何动态更新路由而不重启、如何部署网关集群避免单点、如何防止恶意刷接口。
🔗《最新版 SpringCloud 2025 从入门到实战》系列课程导航
第一部分:微服务前置基础 & 新版环境搭建(第1-5课)
第二部分:注册中心核心(Nacos 最新版)(第6-9课)
第三部分:配置中心核心(Nacos配置中心)(第10-12课)
第四部分:服务通信核心(OpenFeign + LoadBalancer)(第13-16课)
第五部分:网关核心(SpringCloud Gateway 新版)(第17-20课)
第六部分:熔断、限流、降级(Sentinel 新版)(第21-24课)
第七部分:微服务监控、链路追踪、日志体系(第25-28课)
第八部分:微服务高阶特性 & 分布式核心能力(第29-31课)
第九部分:企业级完整项目实战 & 架构复盘(第32-35课)