Spring Cloud easily solves cross-domain issues, don’t use them indiscriminately anymore!

question

In Spring Cloud projects, it is very common to separate front-end and back-end. When debugging, you will encounter two situations of cross-domain:

The front-end page accesses the backend of the microservice through different domain names or IPs. For example, the front-end personnel will start HttpServer locally and directly connect to the backend to develop locally started services. At this time, if no configuration is added, the request of the front-end page will be restricted by the browser across domains. Interception, therefore, business services often add the following code to set global cross-domain settings:

@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);
}

The front-end page accesses SpringCloud Gateway through different domain names or IPs. For example, the front-end personnel can start the HttpServer locally and directly connect to the gateway of the server for debugging. At this time, cross-domain will also be encountered. Need to be added to the Gateway configuration file:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
        # 仅在开发环境设置为*
          '[/**]':
            allowedOrigins: "*"
            allowedHeaders: "*"
            allowedMethods: "*"

So, at this time, the cross-domain problems of direct connection to microservices and gateways have been solved. Isn’t it perfect?

Spring Cloud tutorial recommendation: https://www.javastack.cn/categories/Spring-Cloud/

No~ Here comes the problem, the front end will still report an error: " Multiple 'Access-Control-Allow-Origin' CORS headers are not allowed ."

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.

Look carefully at the returned response header, which contains two Access-Control-Allow-Origin headers.

We use the client version of PostMan to make a simulation, set the header in the request: Origin : *, and check the header of the returned result:

You cannot use the Chrome plug-in version. Due to browser limitations, the Origin header set by the plug-in version is invalid.

Found the problem:

Varyand Access-Control-Allow-Origintwo headers are repeated twice, of which the browser has uniqueness restrictions on the latter!

analyze

Spring Cloud Gateway is based SpringWebFluxon. All web requests are first handed over DispatcherHandlerfor processing, and HTTP requests are handed over to the specifically registered handler for processing.

We know that Spring Cloud Gateway performs request forwarding by configuring routing information in the configuration file. Generally, it uses the url predicates mode, which corresponds to RoutePredicateHandlerMapping. Therefore, DispatcherHandlerthe request will be handed over toRoutePredicateHandlerMapping.

So, let's take a look at RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange)the method. The default provider is its parent class 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;
    });
}

Note:

Some methods on the Internet about modifying Gateway's CORS settings are the same as the previous SpringBoot, implementing a CorsWebFilterBean and providing it by writing code CorsConfigurationinstead of modifying Gateway's configuration file. In fact, the essence is to hand over the configuration to corsProcessor for processing. Different approaches lead to the same goal. But the solution through configuration is always more elegant than hard code.

This method loads all defined in Gateway GlobalFilterand returns it as a handler, but before returning, it performs CORS verification. After obtaining the configuration, it is handed over to corsProcessor for processing, that is, the DefaultCorsProcessorclass

Take a look at DefaultCorsProcessorthe process method below:

@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;
}

You can see that in , DefaultCorsProcessorbased on our configuration in , the and headers appliation.ymlare added to Response .VaryAccess-Control-Allow-Origin

The next step is to enter each GlobalFilter for processing. Among them, NettyRoutingFilteris responsible for actually forwarding the request to the background microservice and obtaining the Response. Focus on the processing results of the filter in the code:

The following headers will be filtered out:

Obviously, in step 3 in the picture, if there are Varyand in the header returned by the background service Access-Control-Allow-Origin, then because it is putAll, it is added without any deduplication, and it will inevitably be repeated. Check the DEBUG result to verify:

confirmed the previous findings.

solve

There are two solutions:

1. Utilize DedupeResponseHeaderconfiguration:

spring:
    cloud:
        gateway:
          globalcors:
            cors-configurations:
              '[/**]':
                allowedOrigins: "*"
                allowedHeaders: "*"
                allowedMethods: "*"
          default-filters:
          - DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST

DedupeResponseHeaderPlus will be enabled later DedupeResponseHeaderGatewayFilterFactoryin which dedupemethods can process values ​​according to a given strategy

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;
  }
 }
  • If the Origin value set in the request is the same as what we set ourselves, for example, the production environment is set to our own domain name xxx.com or the development and test environment is set to * (the Origin value cannot be set in the browser, Even if it is set, it will not work (the browser defaults to the current access address), then you can choose RETAIN_UNIQUEa strategy and return to the front end after deduplication.
  • If the Oringin value set in the request is not the same as what we set ourselves, RETAIN_UNIQUEthe policy will not take effect. For example, "*" and "xxx.com" are two different Origins, and two Access-Control-Allow-Originheaders will eventually be returned. At this time, looking at the code, in the header of the response, the value we configured ourselves is added first Access-Control-Allow-Origin, so we can set the policy RETAIN_FIRSTto only keep the value we set ourselves.

In most cases, what we want to return is the rules we set ourselves, so we RETAIN_FIRSTcan use it directly. In fact, DedupeResponseHeaderrepeated processing can be done for all headers.

2. Manually write one CorsResponseHeaderFilterto GlobalFiltermodify the header in Response.

@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);
                            }
                        }
                    });
        }));
    }
}

There are two things to note here:

1) As can be seen from the figure below, after obtaining the return value, Orderthe larger the value of Filter, the earlier the Response is processed. What actually returns the Response to the front end is that if NettyWriteResponseFilterwe want to modify the Response before it, Orderthe value must be Bigger than NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER.

2) When modifying the post filter, some texts on the Internet are used Mono.deferto do this. This method will start from this filter and re-execute other filters after it. Generally, we will add some authentication or authentication, which needs to be GlobalFilterin The method is used in these filters ServerWebExchangeUtils.isAlreadyRouted(exchange)to determine whether to repeat the operation. Otherwise, the operation may be repeated twice, so it is recommended to use fromRunnableto avoid this situation.

Author: EdisonXu - Xu Yanfei Source: http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html

Recommended recent popular articles:

1. Compilation of 1,000+ Java interview questions and answers (2022 latest version)

2. Explosive! Java coroutines are coming. . .

3. Spring Boot 2.x tutorial, so complete!

4. Stop filling the screen with explosive categories and try the decorator mode. This is the elegant way! !

5. "Java Development Manual (Songshan Edition)" is newly released, download it quickly!

If you think it’s good, don’t forget to like and retweet!

Guess you like

Origin blog.csdn.net/youanyyou/article/details/132870010