Spring Cloud Gateway 2.x跨域时出现重复Origin的BUG问题
作者:持盾的紫眸
这篇文章主要介绍了Spring Cloud Gateway 2.x跨域时出现重复Origin的BUG问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
版本
- Spring Cloud :
Greenwich.SR1
- Spring Cloud Gateway :
2.1.1.RELEASE
现象
跨域时POST请求body内容为空,报跨域失败错误
原因是Access-Control-Allow-Origin只允许有一个值,而响应头里有多个Origin
The ‘Access-Control-Allow-Origin’ header contains multiple values “*, *”, but only one is allowed.
解决方式
@Configuration public class CorsConfiguration { private static final String ALLOWED_HEADERS = "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN,token,username,client"; private static final String ALLOWED_METHODS = "*"; private static final String ALLOWED_ORIGIN = "*"; private static final String ALLOWED_EXPOSE = "*"; private static final String MAX_AGE = "3600"; @Bean public WebFilter corsFilter() { return (ServerWebExchange ctx, WebFilterChain chain) -> { ServerHttpRequest request = ctx.getRequest(); if (CorsUtils.isCorsRequest(request)) { ServerHttpResponse response = ctx.getResponse(); HttpHeaders headers = response.getHeaders(); headers.set("Access-Control-Allow-Origin", ALLOWED_ORIGIN); headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS); headers.add("Access-Control-Max-Age", MAX_AGE); headers.add("Access-Control-Allow-Headers", ALLOWED_HEADERS); headers.add("Access-Control-Expose-Headers", ALLOWED_EXPOSE); headers.add("Access-Control-Allow-Credentials", "true"); if (request.getMethod() == HttpMethod.OPTIONS) { response.setStatusCode(HttpStatus.OK); return Mono.empty(); } } return chain.filter(ctx); }; } } @Component("corsResponseHeaderFilter") public class CorsResponseHeaderFilter implements GlobalFilter, Ordered { @Override public int getOrder() { // 指定此过滤器位于NettyWriteResponseFilter之后 // 即待处理完响应体后接着处理响应头 return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return chain.filter(exchange).then(Mono.defer(() -> { exchange.getResponse().getHeaders().entrySet().stream() .filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1)) .filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) || kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS))) .forEach(kv -> { kv.setValue(new ArrayList<String>() {{ add(kv.getValue().get(0)); }}); }); return chain.filter(exchange); })); } }
原因
Spring Cloud Gateway的NettyRoutingFilter
源码中有BUG
位于182行:
response.getHeaders().putAll(filteredResponseHeaders);
根据github上的issue将在后面几个版本中进行修复
当前版本需要自行解决
- 既然问题出在过滤器链条上,那么还是用Spring的方式,增加一个过滤器,插入到过滤器链条中。不过,新增的这个过滤器在整个链条上的位置有特殊要求。
- 当请求经过NettyRoutingFilter处理后,并不会马上响应客户端请求,接下来还有重要的一步要做,那就是处理响应体(ResponseBoby),由NettyWriteResponseFilter这个过滤器来处理,所以,要修复这个问题,就在处理完响应体之后立马再处理重复的跨域请求头就OK了
/* * Copyright 2013-2019 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.gateway.filter; import java.net.URI; import java.util.List; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.HttpMethod; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.NettyPipeline; import reactor.netty.http.client.HttpClient; import reactor.netty.http.client.HttpClientResponse; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.config.HttpClientProperties; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter; import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter.Type; import org.springframework.cloud.gateway.support.TimeoutException; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.AbstractServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.util.StringUtils; import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter.filterRequest; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_CONN_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CLIENT_RESPONSE_HEADER_NAMES; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.PRESERVE_HOST_HEADER_ATTRIBUTE; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.setAlreadyRouted; /** * @author Spencer Gibb * @author Biju Kunjummen */ public class NettyRoutingFilter implements GlobalFilter, Ordered { private final HttpClient httpClient; private final ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider; private final HttpClientProperties properties; // do not use this headersFilters directly, use getHeadersFilters() instead. private volatile List<HttpHeadersFilter> headersFilters; public NettyRoutingFilter(HttpClient httpClient, ObjectProvider<List<HttpHeadersFilter>> headersFiltersProvider, HttpClientProperties properties) { this.httpClient = httpClient; this.headersFiltersProvider = headersFiltersProvider; this.properties = properties; } public List<HttpHeadersFilter> getHeadersFilters() { if (headersFilters == null) { headersFilters = headersFiltersProvider.getIfAvailable(); } return headersFilters; } @Override public int getOrder() { return Ordered.LOWEST_PRECEDENCE; } @Override @SuppressWarnings("Duplicates") public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { URI requestUrl = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); String scheme = requestUrl.getScheme(); if (isAlreadyRouted(exchange) || (!"http".equals(scheme) && !"https".equals(scheme))) { return chain.filter(exchange); } setAlreadyRouted(exchange); ServerHttpRequest request = exchange.getRequest(); final HttpMethod method = HttpMethod.valueOf(request.getMethodValue()); final String url = requestUrl.toString(); HttpHeaders filtered = filterRequest(getHeadersFilters(), exchange); final DefaultHttpHeaders httpHeaders = new DefaultHttpHeaders(); filtered.forEach(httpHeaders::set); String transferEncoding = request.getHeaders() .getFirst(HttpHeaders.TRANSFER_ENCODING); boolean chunkedTransfer = "chunked".equalsIgnoreCase(transferEncoding); boolean preserveHost = exchange .getAttributeOrDefault(PRESERVE_HOST_HEADER_ATTRIBUTE, false); Flux<HttpClientResponse> responseFlux = this.httpClient .chunkedTransfer(chunkedTransfer).request(method).uri(url) .send((req, nettyOutbound) -> { req.headers(httpHeaders); if (preserveHost) { String host = request.getHeaders().getFirst(HttpHeaders.HOST); req.header(HttpHeaders.HOST, host); } return nettyOutbound.options(NettyPipeline.SendOptions::flushOnEach) .send(request.getBody() .map(dataBuffer -> ((NettyDataBuffer) dataBuffer) .getNativeBuffer())); }).responseConnection((res, connection) -> { ServerHttpResponse response = exchange.getResponse(); // put headers and status so filters can modify the response HttpHeaders headers = new HttpHeaders(); res.responseHeaders().forEach( entry -> headers.add(entry.getKey(), entry.getValue())); String contentTypeValue = headers.getFirst(HttpHeaders.CONTENT_TYPE); if (StringUtils.hasLength(contentTypeValue)) { exchange.getAttributes().put(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR, contentTypeValue); } HttpStatus status = HttpStatus.resolve(res.status().code()); if (status != null) { response.setStatusCode(status); } else if (response instanceof AbstractServerHttpResponse) { // https://jira.spring.io/browse/SPR-16748 ((AbstractServerHttpResponse) response) .setStatusCodeValue(res.status().code()); } else { throw new IllegalStateException( "Unable to set status code on response: " + res.status().code() + ", " + response.getClass()); } // make sure headers filters run after setting status so it is // available in response HttpHeaders filteredResponseHeaders = HttpHeadersFilter.filter( getHeadersFilters(), headers, exchange, Type.RESPONSE); if (!filteredResponseHeaders .containsKey(HttpHeaders.TRANSFER_ENCODING) && filteredResponseHeaders .containsKey(HttpHeaders.CONTENT_LENGTH)) { // It is not valid to have both the transfer-encoding header and // the content-length header // remove the transfer-encoding header in the response if the // content-length header is presen response.getHeaders().remove(HttpHeaders.TRANSFER_ENCODING); } exchange.getAttributes().put(CLIENT_RESPONSE_HEADER_NAMES, filteredResponseHeaders.keySet()); response.getHeaders().putAll(filteredResponseHeaders); // Defer committing the response until all route filters have run // Put client response as ServerWebExchange attribute and write // response later NettyWriteResponseFilter exchange.getAttributes().put(CLIENT_RESPONSE_ATTR, res); exchange.getAttributes().put(CLIENT_RESPONSE_CONN_ATTR, connection); return Mono.just(res); }); if (properties.getResponseTimeout() != null) { responseFlux = responseFlux.timeout(properties.getResponseTimeout(), Mono.error(new TimeoutException("Response took longer than timeout: " + properties.getResponseTimeout()))) .onErrorMap(TimeoutException.class, th -> new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, th.getMessage(), th)); } return responseFlux.then(chain.filter(exchange)); } }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。