En los proyectos de SpringCloud, la separación de front-end y back-end es muy común en la actualidad. Al depurar, encontrará dos casos de dominio cruzado:
La página de front-end accede al backend del microservicio a través de diferentes nombres de dominio o IP
Por ejemplo, el personal de front-end iniciará HttpServer directamente en segundo plano para desarrollar servicios locales. En este momento, si no se agrega ninguna configuración, la solicitud de la página de front-end será interceptada por las restricciones entre dominios del navegador. Por lo tanto, el servicio comercial a menudo agrega el siguiente código para establecer el dominio cruzado global:
@Bean
public CorsFilter corsFilter() {
logger.debug("CORS限制打开");
CorsConfiguration config = new CorsConfiguration();
# 仅在开发环境设置为*
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
Las páginas frontales acceden a SpringCloud Gateway a través de diferentes nombres de dominio o IP
Por ejemplo, el personal de front-end puede depurar la puerta de enlace desde el HttpServer conectado directamente al servidor localmente. En este momento, también se encontrarán dominios cruzados. Debe agregarse al archivo de configuración de Gateway:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
# 仅在开发环境设置为*
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
Entonces, en este momento, el problema entre dominios de conectar directamente microservicios y puertas de enlace se ha resuelto, ¿no es perfecto?
No ~ Aquí viene el problema, el front-end **** seguirá informando un error: "No se permiten múltiples encabezados CORS 'Access-Control-Allow-Origin'" .
Access to XMLHttpRequest at 'http://192.168.2.137:8088/api/two' from origin 'http://localhost:3200' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header contains multiple values '*, http://localhost:3200', but only one is allowed.
Mire cuidadosamente el encabezado de respuesta devuelto, que contiene dos encabezados Access-Control-Allow-Origin.
Usamos la versión del lado del cliente de PostMan para hacer una simulación, establecer el encabezado en la solicitud: Origen: * y ver el encabezado del resultado devuelto:
No se puede utilizar la versión del complemento de Chrome. Debido a las limitaciones del navegador, no es posible configurar el encabezado de origen en la versión del complemento.

Encontré el problema: los encabezados Vary y Access-Control-Allow-Origin se repiten dos veces, ¡y el navegador tiene una restricción única en este último!
analizar
Spring Cloud Gateway se basa en Spring Web Flux.Todas las solicitudes web se entregan primero a DispatcherHandler para su procesamiento, y las solicitudes HTTP se entregan al controlador registrado específicamente para su procesamiento.
Sabemos que Spring Cloud Gateway realiza el reenvío de solicitudes configurando la información de enrutamiento en el archivo de configuración. Generalmente, se usa el modo de predicados de URL, que corresponde a RoutePredicateHandlerMapping. Entonces, DispatcherHandler pasará la solicitud a RoutePredicateHandlerMapping.

Método RoutePredicateHandlerMapping.getHandler(ServerWebExchange Exchange), el proveedor predeterminado es su clase principal AbstractHandlerMapping:
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
return getHandlerInternal(exchange).map(handler -> {
if (logger.isDebugEnabled()) {
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
}
ServerHttpRequest request = exchange.getRequest();
// 可以看到是在这一行就进行CORS判断,两个条件:
// 1. 是否配置了CORS,如果不配的话,默认是返回false的
// 2. 或者当前请求是OPTIONS请求,且头里包含ORIGIN和ACCESS_CONTROL_REQUEST_METHOD
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
config = (config != null ? config.combine(handlerConfig) : handlerConfig);
//此处交给DefaultCorsProcessor去处理了
if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
return REQUEST_HANDLED_HANDLER;
}
}
return handler;
});
}
Hay algunas formas de modificar la configuración de CORS de la puerta de enlace en Internet. Al igual que el SpringBoot anterior, se implementa un CorsWebFilter Bean y CorsConfiguration se proporciona escribiendo código en lugar de modificar el archivo de configuración de la puerta de enlace. De hecho, la esencia es entregar la configuración a corsProcessor para su procesamiento, lo que conduce al mismo objetivo. Pero confiar en la configuración para resolver siempre es más elegante que el código duro.
Este método carga todos los GlobalFilters definidos en la puerta de enlace y los devuelve como controladores, pero antes de regresar, primero se realiza la verificación de CORS y, después de obtener la configuración, se entrega a corsProcessor para su procesamiento, es decir, la clase DefaultCorsProcessor.
Mire el método de proceso de DefaultCorsProcessor
@Override
public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders responseHeaders = response.getHeaders();
List<String> varyHeaders = responseHeaders.get(HttpHeaders.VARY);
if (varyHeaders == null) {
// 第一次进来时,肯定是空,所以加了一次VERY的头,包含ORIGIN, ACCESS_CONTROL_REQUEST_METHOD和ACCESS_CONTROL_REQUEST_HEADERS
responseHeaders.addAll(HttpHeaders.VARY, VARY_HEADERS);
}
else {
for (String header : VARY_HEADERS) {
if (!varyHeaders.contains(header)) {
responseHeaders.add(HttpHeaders.VARY, header);
}
}
}
if (!CorsUtils.isCorsRequest(request)) {
return true;
}
if (responseHeaders.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {
if (preFlightRequest) {
rejectRequest(response);
return false;
}
else {
return true;
}
}
return handleInternal(exchange, config, preFlightRequest);
}
// 在这个类里进行实际的CORS校验和处理
protected boolean handleInternal(ServerWebExchange exchange,
CorsConfiguration config, boolean preFlightRequest) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
HttpHeaders responseHeaders = response.getHeaders();
String requestOrigin = request.getHeaders().getOrigin();
String allowOrigin = checkOrigin(config, requestOrigin);
if (allowOrigin == null) {
logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
rejectRequest(response);
return false;
}
HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
if (allowMethods == null) {
logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
rejectRequest(response);
return false;
}
List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
List<String> allowHeaders = checkHeaders(config, requestHeaders);
if (preFlightRequest && allowHeaders == null) {
logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
rejectRequest(response);
return false;
}
//此处添加了AccessControllAllowOrigin的头
responseHeaders.setAccessControlAllowOrigin(allowOrigin);
if (preFlightRequest) {
responseHeaders.setAccessControlAllowMethods(allowMethods);
}
if (preFlightRequest && !allowHeaders.isEmpty()) {
responseHeaders.setAccessControlAllowHeaders(allowHeaders);
}
if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
}
if (Boolean.TRUE.equals(config.getAllowCredentials())) {
responseHeaders.setAccessControlAllowCredentials(true);
}
if (preFlightRequest && config.getMaxAge() != null) {
responseHeaders.setAccessControlMaxAge(config.getMaxAge());
}
return true;
}
Como puede ver, en DefaultCorsProcessor, de acuerdo con nuestra configuración en application.yml, los encabezados de Vary y Access-Control-Allow-Origin se agregan a Response.

El siguiente paso es ingresar cada GlobalFilter para su procesamiento. NettyRoutingFilter es responsable de reenviar la solicitud al microservicio de fondo y obtener la Respuesta. Concéntrese en la parte del resultado del procesamiento del filtro en el código:
Se filtrarán los siguientes encabezados:

Obviamente, en el tercer paso de la figura, si hay Vary y Access-Control-Allow-Origin en el encabezado devuelto por el servicio en segundo plano, en este momento, debido a que es putAll, se agregará sin ninguna deduplicación y se repetirá Eche un vistazo a los resultados de DEBUG para verificar:
Los hallazgos anteriores fueron verificados.
Hay dos soluciones:
Usar la configuración de DedupeResponseHeader
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
default-filters:
- DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
DedupeResponseHeader más DedupeResponseHeaderGatewayFilterFactory en el que el método de dedupe puede procesar valores de acuerdo con una estrategia determinada.
private void dedupe(HttpHeaders headers, String name, Strategy strategy) {
List<String> values = headers.get(name);
if (values == null || values.size() <= 1) {
return;
}
switch (strategy) {
// 只保留第一个
case RETAIN_FIRST:
headers.set(name, values.get(0));
break;
// 保留最后一个
case RETAIN_LAST:
headers.set(name, values.get(values.size() - 1));
break;
// 去除值相同的
case RETAIN_UNIQUE:
headers.put(name, values.stream().distinct().collect(Collectors.toList()));
break;
default:
break;
}
}
Si el valor de Origen establecido en la solicitud es el mismo que establecemos nosotros, por ejemplo, el entorno de producción se establece con su propio nombre de dominio xxx.com o el entorno de desarrollo y prueba se establece con * (el valor de Origen no puede configurarse en el navegador, la configuración no funciona, el navegador tiene por defecto la dirección de acceso actual), entonces puede usar la estrategia RETAIN_UNIQUE para volver al front-end después de la deduplicación.
Si el valor de Oringin establecido en la solicitud no es el mismo que el establecido por nosotros, la política RETAIN_UNIQUE no tendrá efecto. Por ejemplo, "*" y "xxx.com" son dos orígenes diferentes y, finalmente, dos controles de acceso. -Encabezado Permitir-Origen. En este punto, mirando el código, en el encabezado de la respuesta, primero se agrega el valor de Access-Control-Allow-Origin configurado por nosotros mismos, por lo tanto, podemos establecer la política en RETAIN_FIRST y mantener solo lo que nosotros establecemos.
En la mayoría de los casos, lo que queremos devolver son las reglas que establecimos nosotros mismos, así que solo use RETAIN_FIRST directamente. De hecho, DedupeResponseHeader puede realizar un procesamiento repetido para todos los encabezados.
Escriba manualmente un GlobalFilter de CorsResponseHeaderFilter para modificar el encabezado en Respuesta
@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(CorsResponseHeaderFilter.class);
private static final String ANY = "*";
@Override
public int getOrder() {
// 指定此过滤器位于NettyWriteResponseFilter之后
// 即待处理完响应体后接着处理响应头
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
@Override
@SuppressWarnings("serial")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
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)
|| kv.getKey().equals(HttpHeaders.VARY)))
.forEach(kv ->
{
// Vary只需要去重即可
if(kv.getKey().equals(HttpHeaders.VARY))
kv.setValue(kv.getValue().stream().distinct().collect(Collectors.toList()));
else{
List<String> value = new ArrayList<>();
if(kv.getValue().contains(ANY)){ //如果包含*,则取*
value.add(ANY);
kv.setValue(value);
}else{
value.add(kv.getValue().get(0)); // 否则默认取第一个
kv.setValue(value);
}
}
});
}));
}
}
Hay dos cosas a tener en cuenta aquí:
-
Como se puede ver en la figura a continuación, después de obtener el valor de retorno, cuanto mayor sea el valor de Orden del Filtro, más se procesará primero la Respuesta, y el NettyWriteResponseFilter que realmente devuelve la Respuesta al front-end es el NettyWriteResponseFilter. desea modificar la Respuesta anterior, el valor de la Orden debe ser mayor que NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER.

-
Al modificar el filtro de publicación, algunos blogs en Internet usan Mono.defer para hacerlo. Este método comenzará desde este filtro y volverá a ejecutar otros filtros detrás de él. Generalmente, agregaremos alguna autenticación o autenticación GlobalFilter, solo es necesario usar el método ServerWebExchangeUtils.isAlreadyRouted(exchange) en estos filtros para determinar si repetir la ejecución, de lo contrario se puede realizar la segunda repetición, por lo que se recomienda usar fromRunnable para evitar esta situación.
Reimpreso de: Edison Xu
Enlace: http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html
¡Pesado! Se ha establecido un grupo de intercambio de programadores
El funcionamiento de la cuenta oficial es inseparable del apoyo de nuestros amigos.
Con el fin de proporcionar una plataforma para que los socios pequeños se comuniquen entre sí, se abrió especialmente un grupo de intercambio de programadores.
Hay muchos maestros técnicos en el grupo, que compartirán algunos puntos técnicos de vez en cuando, y algunos recolectores de recursos compartirán algunos materiales de aprendizaje de alta calidad de vez en cuando. (¡El grupo es completamente gratis, sin anuncios ni clases!)
Los amigos que necesiten unirse al grupo pueden presionar prolongadamente para escanear el código QR a continuación.
▲Presione prolongadamente para escanear el código