Spring Cloud 轻松解决跨域,别再乱用了!

打印 上一主题 下一主题

主题 988|帖子 988|积分 2964

问题

在Spring Cloud项目中,前后端分离目前很常见,在调试时,会遇到两种情况的跨域:
前端页面通过不同域名或IP访问微服务的后台,例如前端人员会在本地起HttpServer 直连后台开发本地起的服务,此时,如果不加任何配置,前端页面的请求会被浏览器跨域限制拦截,所以,业务服务常常会添加如下代码设置全局跨域:
  1. @Bean
  2. public CorsFilter corsFilter() {
  3.     logger.debug("CORS限制打开");
  4.     CorsConfiguration config = new CorsConfiguration();
  5.     # 仅在开发环境设置为*
  6.     config.addAllowedOrigin("*");
  7.     config.addAllowedHeader("*");
  8.     config.addAllowedMethod("*");
  9.     config.setAllowCredentials(true);
  10.     UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
  11.     configSource.registerCorsConfiguration("/**", config);
  12.     return new CorsFilter(configSource);
  13. }
复制代码
前端页面通过不同域名或IP访问SpringCloud Gateway,例如前端人员在本地起HttpServer直连服务器的Gateway进行调试。此时,同样会遇到跨域。需要在Gateway的配置文件中增加:
  1. spring:
  2.   cloud:
  3.     gateway:
  4.       globalcors:
  5.         cors-configurations:
  6.         # 仅在开发环境设置为*
  7.           '[/**]':
  8.             allowedOrigins: "*"
  9.             allowedHeaders: "*"
  10.             allowedMethods: "*"
复制代码
那么,此时直连微服务和网关的跨域问题都解决了,是不是很完美?
Spring Cloud 教程推荐:https://www.javastack.cn/categories/Spring-Cloud/
No~ 问题来了,前端仍然会报错:“不允许有多个’Access-Control-Allow-Origin’ CORS头”。
  1. Access to XMLHttpRequest at 'http://192.168.2.137:8088/api/two' from origin 'http://localhost:3200' has been blocked by CORS policy:
  2. 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的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 :
  1. @Override
  2. public Mono<Object> getHandler(ServerWebExchange exchange) {
  3.     return getHandlerInternal(exchange).map(handler -> {
  4.         if (logger.isDebugEnabled()) {
  5.             logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
  6.         }
  7.         ServerHttpRequest request = exchange.getRequest();
  8.         // 可以看到是在这一行就进行CORS判断,两个条件:
  9.         // 1. 是否配置了CORS,如果不配的话,默认是返回false的
  10.         // 2. 或者当前请求是OPTIONS请求,且头里包含ORIGIN和ACCESS_CONTROL_REQUEST_METHOD
  11.         if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
  12.             CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
  13.             CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
  14.             config = (config != null ? config.combine(handlerConfig) : handlerConfig);
  15.             //此处交给DefaultCorsProcessor去处理了
  16.             if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
  17.                 return REQUEST_HANDLED_HANDLER;
  18.             }
  19.         }
  20.         return handler;
  21.     });
  22. }
复制代码
注:
网上有些关于修改Gateway的CORS设定的方式,是跟前面SpringBoot一样,实现一个CorsWebFilter的Bean,靠写代码提供 CorsConfiguration ,而不是修改Gateway的配置文件。其实本质,都是将配置交给corsProcessor去处理,殊途同归。但靠配置解决永远比hard code来的优雅。
该方法把Gateway里定义的所有的 GlobalFilter 加载进来,作为handler返回,但在返回前,先进行CORS校验,获取配置后,交给corsProcessor去处理,即DefaultCorsProcessor类
看下DefaultCorsProcessor的process方法:
  1. @Override
  2. public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {
  3.     ServerHttpRequest request = exchange.getRequest();
  4.     ServerHttpResponse response = exchange.getResponse();
  5.     HttpHeaders responseHeaders = response.getHeaders();
  6.     List<String> varyHeaders = responseHeaders.get(HttpHeaders.VARY);
  7.     if (varyHeaders == null) {
  8.         // 第一次进来时,肯定是空,所以加了一次VERY的头,包含ORIGIN, ACCESS_CONTROL_REQUEST_METHOD和ACCESS_CONTROL_REQUEST_HEADERS
  9.         responseHeaders.addAll(HttpHeaders.VARY, VARY_HEADERS);
  10.     }
  11.     else {
  12.         for (String header : VARY_HEADERS) {
  13.             if (!varyHeaders.contains(header)) {
  14.                 responseHeaders.add(HttpHeaders.VARY, header);
  15.             }
  16.         }
  17.     }
  18.     if (!CorsUtils.isCorsRequest(request)) {
  19.         return true;
  20.     }
  21.     if (responseHeaders.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {
  22.         logger.trace("Skip: response already contains "Access-Control-Allow-Origin"");
  23.         return true;
  24.     }
  25.     boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
  26.     if (config == null) {
  27.         if (preFlightRequest) {
  28.             rejectRequest(response);
  29.             return false;
  30.         }
  31.         else {
  32.             return true;
  33.         }
  34.     }
  35.     return handleInternal(exchange, config, preFlightRequest);
  36. }
  37. // 在这个类里进行实际的CORS校验和处理
  38. protected boolean handleInternal(ServerWebExchange exchange,
  39.                                  CorsConfiguration config, boolean preFlightRequest) {
  40.     ServerHttpRequest request = exchange.getRequest();
  41.     ServerHttpResponse response = exchange.getResponse();
  42.     HttpHeaders responseHeaders = response.getHeaders();
  43.     String requestOrigin = request.getHeaders().getOrigin();
  44.     String allowOrigin = checkOrigin(config, requestOrigin);
  45.     if (allowOrigin == null) {
  46.         logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");
  47.         rejectRequest(response);
  48.         return false;
  49.     }
  50.     HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
  51.     List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
  52.     if (allowMethods == null) {
  53.         logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");
  54.         rejectRequest(response);
  55.         return false;
  56.     }
  57.     List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
  58.     List<String> allowHeaders = checkHeaders(config, requestHeaders);
  59.     if (preFlightRequest && allowHeaders == null) {
  60.         logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");
  61.         rejectRequest(response);
  62.         return false;
  63.     }
  64.     //此处添加了AccessControllAllowOrigin的头
  65.     responseHeaders.setAccessControlAllowOrigin(allowOrigin);
  66.     if (preFlightRequest) {
  67.         responseHeaders.setAccessControlAllowMethods(allowMethods);
  68.     }
  69.     if (preFlightRequest && !allowHeaders.isEmpty()) {
  70.         responseHeaders.setAccessControlAllowHeaders(allowHeaders);
  71.     }
  72.     if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
  73.         responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
  74.     }
  75.     if (Boolean.TRUE.equals(config.getAllowCredentials())) {
  76.         responseHeaders.setAccessControlAllowCredentials(true);
  77.     }
  78.     if (preFlightRequest && config.getMaxAge() != null) {
  79.         responseHeaders.setAccessControlMaxAge(config.getMaxAge());
  80.     }
  81.     return true;
  82. }
复制代码
可以看到,在DefaultCorsProcessor 中,根据我们在appliation.yml 中的配置,给Response添加了 Vary 和 Access-Control-Allow-Origin 的头。

再接下来就是进入各个GlobalFilter进行处理了,其中NettyRoutingFilter 是负责实际将请求转发给后台微服务,并获取Response的,重点看下代码中filter的处理结果的部分:

其中以下几种header会被过滤掉的:

很明显,在图里的第3步中,如果后台服务返回的header里有 Vary 和 Access-Control-Allow-Origin ,这时由于是putAll,没有做任何去重就加进去了,必然会重复,看看DEBUG结果验证一下:

验证了前面的发现。
解决

解决的方案有两种:
1. 利用 DedupeResponseHeader 配置:
  1. spring:
  2.     cloud:
  3.         gateway:
  4.           globalcors:
  5.             cors-configurations:
  6.               '[/**]':
  7.                 allowedOrigins: "*"
  8.                 allowedHeaders: "*"
  9.                 allowedMethods: "*"
  10.           default-filters:
  11.           - DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
复制代码
DedupeResponseHeader 加上以后会启用DedupeResponseHeaderGatewayFilterFactory 在其中,dedupe方法可以按照给定策略处理值
  1. private void dedupe(HttpHeaders headers, String name, Strategy strategy) {
  2.   List<String> values = headers.get(name);
  3.   if (values == null || values.size() <= 1) {
  4.    return;
  5.   }
  6.   switch (strategy) {
  7.   // 只保留第一个
  8.   case RETAIN_FIRST:
  9.    headers.set(name, values.get(0));
  10.    break;
  11.   // 保留最后一个
  12.   case RETAIN_LAST:
  13.    headers.set(name, values.get(values.size() - 1));
  14.    break;
  15.   // 去除值相同的
  16.   case RETAIN_UNIQUE:
  17.    headers.put(name, values.stream().distinct().collect(Collectors.toList()));
  18.    break;
  19.   default:
  20.    break;
  21.   }
  22. }
复制代码
此处有两个地方要注意:
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 避免这种情况。
作者:EdisonXu - 徐焱飞
来源:http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html
近期热文推荐:
1.1,000+ 道 Java面试题及答案整理(2022最新版)
2.劲爆!Java 协程要来了。。。
3.Spring Boot 2.x 教程,太全了!
4.别再写满屏的爆爆爆炸类了,试试装饰器模式,这才是优雅的方式!!
5.《Java开发手册(嵩山版)》最新发布,速速下载!
觉得不错,别忘了随手点赞+转发哦!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

笑看天下无敌手

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表