В проектах 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 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);
}
}
});
}));
}
}
Здесь следует отметить две вещи:
-
Как видно из рисунка ниже, после получения возвращаемого значения, чем больше значение Order фильтра, тем больше сначала обрабатывается Response, а NettyWriteResponseFilter, который действительно возвращает Response во внешний интерфейс, является NettyWriteResponseFilter. хотите изменить ответ перед ним, значение заказа должно быть больше, чем NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER.

-
При изменении фильтра сообщений некоторые блоги в Интернете используют для этого Mono.defer.Этот метод будет запускаться из этого фильтра и повторно выполнять другие фильтры за ним.Как правило, мы добавим некоторую аутентификацию или аутентификацию GlobalFilter, просто это необходимо использовать метод ServerWebExchangeUtils.isAlreadyRouted(exchange) в этих фильтрах, чтобы определить, следует ли повторять выполнение, иначе может быть выполнено второе повторение, поэтому рекомендуется использовать fromRunnable, чтобы избежать этой ситуации.
Перепечатано из: Эдисон Сюй
Ссылка: http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html
Тяжелый! Создана группа по обмену программистами
Работа официального аккаунта неотделима от поддержки наших друзей.
Для того, чтобы предоставить площадку для общения небольших партнеров друг с другом, была специально открыта группа по обмену программистами.
В группе есть много технических мастеров, которые время от времени будут делиться некоторыми техническими моментами, а некоторые сборщики ресурсов время от времени будут делиться некоторыми высококачественными учебными материалами. (Группа полностью бесплатная, без рекламы и без занятий!)
Друзья, которым нужно присоединиться к группе, могут долго нажимать, чтобы отсканировать QR-код ниже.
▲Длительное нажатие для сканирования кода