В проектах SpringCloud в настоящее время очень распространено разделение front-end и back-end.При отладке вы столкнетесь с двумя случаями междоменного взаимодействия:

Интерфейсная страница обращается к серверной части микросервиса через разные доменные имена или IP-адреса.

Например, персонал внешнего интерфейса запустит HttpServer непосредственно в фоновом режиме для разработки локальных служб. В это время, если не будет добавлена ​​​​конфигурация, запрос страницы внешнего интерфейса будет перехвачен междоменными ограничениями браузера. Таким образом, бизнес-служба часто добавляет следующий код для установки глобального перекрестного домена:

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

Интерфейсные страницы получают доступ к SpringCloud Gateway через разные доменные имена или IP-адреса.

Например, внешний персонал может отлаживать шлюз с HttpServer, напрямую подключенного к локальному серверу. В это время также будут встречаться междоменные. Его нужно добавить в файл конфигурации шлюза:

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

Итак, на данный момент междоменная проблема прямого подключения микросервисов и шлюзов решена, разве это не идеально?

Нет~ Здесь возникает проблема, внешний интерфейс **** по-прежнему будет сообщать об ошибке: «Несколько заголовков 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.

Внимательно посмотрите на возвращенный заголовок ответа, который содержит два заголовка Access-Control-Allow-Origin.

Используем клиентскую версию PostMan для симуляции, устанавливаем заголовок в запросе: Origin: * и просматриваем заголовок возвращаемого результата:

Невозможно использовать версию подключаемого модуля Chrome. Из-за ограничений браузера установка заголовка Origin в версии подключаемого модуля недействительна.

картина

Обнаружил проблему: дважды повторяются заголовки Vary и Access-Control-Allow-Origin, а на последний браузер имеет уникальное ограничение!

анализировать

Spring Cloud Gateway основан на Spring Web Flux.Все веб-запросы сначала передаются DispatcherHandler для обработки, а HTTP-запросы передаются специально зарегистрированному обработчику для обработки.

Мы знаем, что Spring Cloud Gateway перенаправляет запросы, настраивая информацию о маршрутизации в файле конфигурации, обычно используя режим предикатов URL, который соответствует RoutePredicateHandlerMapping. Итак, DispatcherHandler передаст запрос в RoutePredicateHandlerMapping.

картина
картина

RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange), поставщиком по умолчанию является его родительский класс 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;
  });
 }

Существует несколько способов изменить настройки CORS шлюза в Интернете.Как и в предыдущем SpringBoot, реализован компонент CorsWebFilter Bean, а CorsConfiguration предоставляется путем написания кода вместо изменения файла конфигурации шлюза. По сути, суть в том, чтобы передать конфигурацию в corsProcessor для обработки, что приводит к той же цели. Но полагаться на конфигурацию для решения всегда более элегантно, чем на жесткий код.

Этот метод загружает все GlobalFilters, определенные в Gateway, и возвращает их как обработчики, но перед возвратом сначала выполняется проверка CORS, а после получения конфигурации она передается на обработку в corsProcessor, а именно в класс DefaultCorsProcessor.

Посмотрите на метод процесса 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;
}

Как видите, в DefaultCorsProcessor, согласно нашей конфигурации в application.yml, в Response добавляются заголовки Vary и Access-Control-Allow-Origin.

картина
картина

Следующим шагом является ввод каждого GlobalFilter для обработки.NettyRoutingFilter отвечает за фактическую пересылку запроса в фоновый микросервис и получение Response.Сосредоточьтесь на части результата обработки фильтра в коде:

картина

Будут отфильтрованы следующие заголовки:

картина
картина

Очевидно, что на третьем шаге на рисунке, если в заголовке, возвращаемом фоновой службой, есть Vary и Access-Control-Allow-Origin, то в это время, поскольку это putAll, он будет добавлен без какой-либо дедупликации, и это будет повторяться. Взгляните на результаты DEBUG, чтобы проверить:

картинаПредыдущие выводы подтвердились.

Есть два решения:

Использовать конфигурацию 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 плюс DedupeResponseHeaderGatewayFilterFactory, в котором метод дедупликации может обрабатывать значения в соответствии с заданной стратегией.

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

Если значение Origin, установленное в запросе, такое же, как и у нас, например, для рабочей среды задано собственное доменное имя xxx.com или для среды разработки и тестирования установлено значение * (значение Origin не может быть установлен в браузере, настройка не работает, браузер по умолчанию использует текущий адрес доступа), то вы можете использовать стратегию RETAIN_UNIQUE, чтобы вернуться к внешнему интерфейсу после дедупликации.

Если значение Oringin, установленное в запросе, не совпадает с установленным нами, политика RETAIN_UNIQUE не вступит в силу. Например, «*» и «xxx.com» — это два разных источника и, в конечном итоге, два контроля доступа. будет возвращен заголовок -Allow-Origin. На этом этапе, глядя на код, в заголовке ответа сначала добавляется настроенное нами значение Access-Control-Allow-Origin, поэтому мы можем установить политику RETAIN_FIRST и оставить только то, что установили сами.

В большинстве случаев мы хотим вернуть правила, которые установили сами, поэтому просто используйте RETAIN_FIRST напрямую. Фактически, DedupeResponseHeader может выполнять повторную обработку для всех заголовков.

Вручную напишите GlobalFilter CorsResponseHeaderFilter, чтобы изменить заголовок в ответе.

@Component
public class CorsResponseHeaderFilter implements GlobalFilterOrdered {

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

Здесь следует отметить две вещи:

  1. Как видно из рисунка ниже, после получения возвращаемого значения, чем больше значение Order фильтра, тем больше сначала обрабатывается Response, а NettyWriteResponseFilter, который действительно возвращает Response во внешний интерфейс, является NettyWriteResponseFilter. хотите изменить ответ перед ним, значение заказа должно быть больше, чем NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER.
картина
картина
  1. При изменении фильтра сообщений некоторые блоги в Интернете используют для этого Mono.defer.Этот метод будет запускаться из этого фильтра и повторно выполнять другие фильтры за ним.Как правило, мы добавим некоторую аутентификацию или аутентификацию GlobalFilter, просто это необходимо использовать метод ServerWebExchangeUtils.isAlreadyRouted(exchange) в этих фильтрах, чтобы определить, следует ли повторять выполнение, иначе может быть выполнено второе повторение, поэтому рекомендуется использовать fromRunnable, чтобы избежать этой ситуации.

Перепечатано из: Эдисон Сюй

Ссылка: http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html

Тяжелый! Создана группа по обмену программистами


Работа официального аккаунта неотделима от поддержки наших друзей.


Для того, чтобы предоставить площадку для общения небольших партнеров друг с другом, была специально открыта группа по обмену программистами.


В группе есть много технических мастеров, которые время от времени будут делиться некоторыми техническими моментами, а некоторые сборщики ресурсов время от времени будут делиться некоторыми высококачественными учебными материалами. (Группа полностью бесплатная, без рекламы и без занятий!)


Друзья, которым нужно присоединиться к группе, могут долго нажимать, чтобы отсканировать QR-код ниже.


картина


▲Длительное нажатие для сканирования кода