在Spring Cloud工程项目中,其间端分立现阶段很常用,在增容时,会碰到三种情形的布吕马:
后端网页透过相同搜索引擎或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); returnnew CorsFilter(configSource); }后端网页透过相同搜索引擎或IP出访SpringCloud Gateway,比如后端相关人员在邻近地区起HttpServer洛佐韦服务工程项目器的Gateway展开增容。这时,反之亦然会碰到布吕马。须要在Gateway的命令行中减少:
spring: cloud: gateway: globalcors: cors-configurations: # 仅在合作开发自然环境增设为* [/**]: allowedOrigins:“*” allowedHeaders: “*” allowedMethods: “*”那么,这时洛佐韦微服务工程项目和网关的布吕马问题都化解了,是不是很完美?No~ 问题来了,后端仍然会报错:“不允许有多个’Access-Control-Allow-Origin’ CORS头”。
Access to XMLHttpRequest at http://192.168.2.137:8088/api/two fromoriginhttp://localhost:3200 has been blocked by CORS policy: The Access-Control-Allow-Originheader contains multiple values*, http://localhost:3200, but only one is allowed.仔细查看返回的响应头,里面包含了两份Access-Control-Allow-Origin头。
我们用客户端版的PostMan做一个模拟,在允诺里增设头:Origin : * ,查看返回结果的头:
不能用Chrome插件版,由于应用程序的管制,插件版增设Origin的Header是无效的
发现问题了:
Vary 和 Access-Control-Allow-Origin 两个头重复了两次,其中应用程序对后者有唯一性管制!
分析
Spring Cloud Gateway是基于SpringWebFlux的,所有web允诺首先是交给DispatcherHandler展开处理的,将HTTP允诺交给具体注册的handler去处理。
我们知道Spring Cloud Gateway展开允诺转发,是在命令行里实用性路由信息,一般都是用url predicates模式,对应的就是RoutePredicateHandlerMapping 。因此,DispatcherHandler会把允诺交给 RoutePredicateHandlerMapping.
那么,接下来看下 RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange)方法,默认提供者是其父类AbstractHandlerMapping :
@Override publicMono<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; }); }注:
网上有些关于修改Gateway的CORS设定的方式,是跟前面SpringBoot一样,实现一个CorsWebFilter的Bean,靠写标识符提供 CorsConfiguration,而不是修改Gateway的命令行。其实本质,都是将实用性交给corsProcessor去处理,殊途同归。但靠实用性化解永远比hard code来的优雅。
该方法把Gateway里定义的所有的 GlobalFilter加载进来DefaultCorsProcessor类
看下DefaultCorsProcessor的process方法:
@Override public boolean process(@NullableCorsConfiguration 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_HEADERSresponseHeaders.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校验和处理 protectedboolean 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 中,根据我们在appliation.yml中的实用性,给Response加进了Vary 和 Access-Control-Allow-Origin 的头。
再接下来就是进入各个GlobalFilter展开处理了,其中NettyRoutingFilter是负责实际将允诺转发给
其中以下几种header会被过滤掉的:
很明显,在图里的第3步中,假如前台服务工程项目返回的header里有 Vary 和 Access-Control-Allow-Origin ,这时由于是putAll,没有做任何人去重就加进去了,必然会重复,看看DEBUG结果验证一下:
验证了前面的发现。
化解
化解的方案有三种:
1. 利用DedupeResponseHeader实用性:
spring: cloud: gateway: globalcors: cors-configurations: [/**]: allowedOrigins: “*” allowedHeaders: “*” allowedMethods: “*” default-filters: – DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRSTDedupeResponseHeader 加上以后会启用DedupeResponseHeaderGatewayFilterFactory 在其中,dedupe方法可以按照给定策略处理。
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; // 去除值相同的 caseRETAIN_UNIQUE: headers.put(name, values.stream().distinct().collect(Collectors.toList()));break; default: break; } }假如允诺中增设的Origin的值与我们自己增设的是同一个,比如生产自然环境增设的都是自己的搜索引擎xxx.com或者合作开发测试自然环境增设的都是*(应用程序中是无法增设Origin的值,增设了也不起作用,应用程序默认是当前出访地址),那么可以选用RETAIN_UNIQUE策略,去重后返回到后端。假如允诺中增设的Oringin的值与我们自己增设的不是同一个,RETAIN_UNIQUE策略就无法生效,比如 ”*“ 和 ”xxx.com“是两个不一样的Origin,最终还是会返回两个Access-Control-Allow-Origin的头。这时,看标识符里,response的header里,先加入的是我们自己实用性的Access-Control-Allow-Origin的值,因此,我们可以将策略增设为RETAIN_FIRST ,只保留我们自己增设的。大多数情形下,我们想要返回的是我们自己增设的规则,因此直接使用RETAIN_FIRST 即可。实际上,DedupeResponseHeader 可以针对所有头,做重复的处理。
2. 手动写一个CorsResponseHeaderFilter的GlobalFilter去修改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) { returnchain.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)根据下图可以看到,在取得返回值后,Filter的Order 值越大,越先处理Response,而真正将Response返回到后端的,是 NettyWriteResponseFilter, 我们要想在它之前修改Response,则Order 的值必须比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER大。
2)修改后置filter时,网上有些文字使用的是 Mono.defer去做的,这种做法,会从此filter开始,重新执行一遍它后面的其他filter,一般我们会加进一些认证或鉴权的 GlobalFilter ,就须要在这些filter里用ServerWebExchangeUtils.isAlreadyRouted(exchange) 方法去判断是否重复执行,否则可能会执行二次重复操作,因此建议使用fromRunnable 避免这种情形。
http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html