跨域和请求体,为什么总在网关翻车
网关是所有流量的入口,跨域响应头、鉴权、日志、请求体校验这些“横切”逻辑最后都会堆到这里。两件事最容易出问题:一是 CORS 头被加了两次,浏览器直接拒绝;二是过滤器想读请求体做校验或签名,读完下游就收不到了。这两个问题的共同点是——报错信息都不指向真正的原因。
跨域:三处配置只留一处
CORS 的规则是“响应里只能有一组 Access-Control-Allow-* 头”。但现实里常见三处都在配:网关的 globalcors、下游服务的 @CrossOrigin、Nginx 的 add_header。结果就是浏览器看到重复头,判定配置非法。
推荐只在网关配,下游一律不配:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: "https://*.example.com,http://localhost:5173"
allowed-methods: [GET, POST, PUT, DELETE, OPTIONS]
allowed-headers: "*"
exposed-headers: [X-Trace-Id]
allow-credentials: true
max-age: 3600
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
两个细节值得单独说。第一,allow-credentials: true 时不能用 allowed-origins: "*",浏览器会拒绝通配符,必须用 allowed-origin-patterns 或明确列出域名。第二,DedupeResponseHeader 是兜底,不是许可证——它能消掉重复头,但消不掉“两边规则不一致”导致的偶发失败,该清理的下游配置还是要清理。
预检请求为什么总被 401
浏览器的预检 OPTIONS 请求不会带 Authorization 头,也不带业务参数。如果鉴权过滤器排在 CORS 处理之前,预检就会被拦成 401/403,控制台里表现为“跨域错误”,实际是鉴权错误。
处理方式有两种:把 CORS 处理器的顺序调到鉴权之前,或者在鉴权过滤器里显式放行预检:
if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
return chain.filter(exchange);
}
CorsUtils 要引 WebFlux 包下的 org.springframework.web.cors.reactive.CorsUtils。只判断 OPTIONS 方法还不够,因为某些简单的 OPTIONS 请求并不是预检;用 isPreFlightRequest 更准确。
请求体只能读一次
WebFlux 的请求体是 Flux<DataBuffer>,设计上就是一次性流。过滤器里调一次 request.getBody(),下游的 @RequestBody 就只能拿到空内容,日志里常见的报错是 Only one connection receive subscriber allowed。
要读就得缓存,并且把缓存后的请求重新包装回去。核心是三点:把 body 读成 byte[]、释放 DataBuffer、用一个 ServerHttpRequestDecorator 让下游还能再读一次:
public class CachedBodyRequest extends ServerHttpRequestDecorator {
private final byte[] body;
private final DataBufferFactory factory = new DefaultDataBufferFactory();
public CachedBodyRequest(ServerHttpRequest delegate, byte[] body) {
super(delegate);
this.body = body;
}
@Override
public Flux<DataBuffer> getBody() {
return Flux.defer(() -> Mono.just(factory.wrap(body)));
}
}
过滤器里用 DataBufferUtils.join(request.getBody()) 拿到完整缓冲区,读成字节数组后必须 DataBufferUtils.release(buffer),否则堆外内存会慢慢涨上去。然后把字节数组放进 exchange 属性供后续过滤器使用,再 exchange.mutate().request(new CachedBodyRequest(request, bytes)).build() 继续调用链。
还要注意两个边界:一是限制缓存大小,spring.codec.max-in-memory-size 默认只有 256KB,超过会抛 DataBufferLimitException;二是 Content-Length 在改写 body 后要同步更新,否则下游可能截断或挂起。
常见坑与排查
坑一:Access-Control-Allow-Origin 是 * 但带了 Cookie。 浏览器不会把凭证发给通配符来源,必须回显具体 Origin 并加 Vary: Origin,否则 CDN 或代理缓存会串源。
坑二:只配了 allowed-headers,忘了 exposed-headers。 前端拿不到 X-Trace-Id、Content-Disposition 这类自定义响应头,以为后端没返回。
坑三:预检缓存时间太短。 max-age 设成 0 或很小,每个请求前都飞一次 OPTIONS,网关 QPS 凭空多一倍。业务允许的话设到 1800 秒以上。
坑四:读 body 的过滤器 order 太靠后。 请求已经转发给下游了才去读,读到的只能是空流。缓存 body 的过滤器要放在路由过滤器之前,通常用 Ordered.HIGHEST_PRECEDENCE + 20 左右。
坑五:路由谓词把 OPTIONS 挡在外面。 如果路由只写了 Method=GET,POST,预检请求会因为不匹配谓词而返回 404,浏览器同样报跨域失败。要么给路由加上 OPTIONS,要么把 CORS 配置放在 globalcors 里由全局处理器接管,让预检根本不进路由匹配。
排查时按这个顺序:浏览器 Network 面板看预检请求的状态码和响应头,确认是 401 还是 CORS 头缺失;用 curl -H "Origin: https://x.example.com" -i 直接看网关返回的头有几组;再打开网关 DEBUG 日志确认过滤器执行顺序。跨域问题九成能在响应头里直接看出来,不需要猜。
评论