Spring Web Reactive Ajax 跨域的坑

原创
2019/11/29 01:27
阅读数 339

吐槽一下,在使用 spring cloud gateway 作为网关时遇到的 ajax 跨域一坑爹的问题。

spring-cloud-gateway 使用 org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping 进行路由匹配;而 RoutePredicateHandlerMapping 由集成 org.springframework.web.reactive.handler.AbstractHandlerMapping。

AbstractHandlerMapping 中 getHandler 的代码如下。

public Mono<Object> getHandler(ServerWebExchange exchange) {
   return getHandlerInternal(exchange).map(handler -> {
      if (logger.isDebugEnabled()) {
         logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
      }
      if (CorsUtils.isCorsRequest(exchange.getRequest())) {
         CorsConfiguration configA = this.corsConfigurationSource.getCorsConfiguration(exchange);
         CorsConfiguration configB = getCorsConfiguration(handler, exchange);
         CorsConfiguration config = (configA != null ? configA.combine(configB) : configB);
         if (!getCorsProcessor().process(config, exchange) ||
               CorsUtils.isPreFlightRequest(exchange.getRequest())) {
            return REQUEST_HANDLED_HANDLER;
         }
      }
      return handler;
   });
}

首先 org.springframework.web.cors.reactive.CorsUtils.isCorsRequest() 方法会判断是否为 cors 请求,判断条件就是 request header 中有 Origin,代码如下:

public static boolean isCorsRequest(ServerHttpRequest request) {
   return (request.getHeaders().get(HttpHeaders.ORIGIN) != null);
}

然后会调用 CorsProcessor 的 process 方法,而 org.springframework.web.reactive.handler.AbstractHandlerMapping 中,默认初始化属性 corsProcessor 是 org.springframework.web.cors.reactive.DefaultCorsProcessor。

接下来看一下 DefaultCorsProcessor 类 process 方法的实现,具体代码如下:

public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {

   ServerHttpRequest request = exchange.getRequest();
   ServerHttpResponse response = exchange.getResponse();

   if (!CorsUtils.isCorsRequest(request)) {
      return true;
   }

   if (responseHasCors(response)) {
      logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");
      return true;
   }

   if (CorsUtils.isSameOrigin(request)) {
      logger.trace("Skip: request is from same 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 请求,不是的话则直接返回。

然后再判断 response header 中是否有 Access-Control-Allow-Origin(记住:不是 spring-cloud-gateway 后端应用的响应头,而是 spring-cloud-gateway 设置的响应头),如果有则直接返回。

接着再调用 org.springframework.web.cors.reactive.CorsUtils.isSameOrigin() 方法判断请求url的 scheme、host、port 是否和 request header Origin 的 scheme、host、port 一致(既是否为同域),一致的话则直接返回;代码如下:

public static boolean isSameOrigin(ServerHttpRequest request) {
   String origin = request.getHeaders().getOrigin();
   if (origin == null) {
      return true;
   }

   URI uri = request.getURI();
   String actualScheme = uri.getScheme();
   String actualHost = uri.getHost();
   int actualPort = getPort(uri.getScheme(), uri.getPort());
   Assert.notNull(actualScheme, "Actual request scheme must not be null");
   Assert.notNull(actualHost, "Actual request host must not be null");
   Assert.isTrue(actualPort != -1, "Actual request port must not be undefined");

   UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
   return (actualScheme.equals(originUrl.getScheme()) &&
         actualHost.equals(originUrl.getHost()) &&
         actualPort == getPort(originUrl.getScheme(), originUrl.getPort()));
}

接下来在调用 org.springframework.web.cors.reactive.CorsUtils.CorsUtils.isPreFlightRequest() 方法 判断是 flight request。

public static boolean isPreFlightRequest(ServerHttpRequest request) {
   return (request.getMethod() == HttpMethod.OPTIONS && isCorsRequest(request) &&
         request.getHeaders().get(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
}

即如果请求方法是 OPTIONS,且 request header 含有 Origin 和 Access-Control-Request-Method,则为 flight request。

当 DefaultCorsProcessor 的方法 process 的参数 CorsConfiguration config 为 null,且为 flight request 是,则会返回 403.

protected void rejectRequest(ServerHttpResponse response) {
   response.setStatusCode(HttpStatus.FORBIDDEN);
}

当,通过 spring.cloud.gateway.globalcors.cors-configurations 根据 PATH 设置了 cors 或者通过 spring.cloud.gateway.routes.ServiceNAME.cors-configurations 设置了路由的 cors,会调用 DefaultCorsProcessor 的 handleInternal 方法。如果是 flight request,则会验证 request header 中的 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 是否在允许的范围内。当其中任何一项未验证通过时,则返回 403;当都验证通过时,则会把服务端配置 cors 在 response header 中返回给客户端。

protected boolean handleInternal(ServerWebExchange exchange,
      CorsConfiguration config, boolean preFlightRequest) {

   ServerHttpRequest request = exchange.getRequest();
   ServerHttpResponse response = exchange.getResponse();
   HttpHeaders responseHeaders = response.getHeaders();

   response.getHeaders().addAll(HttpHeaders.VARY, Arrays.asList(HttpHeaders.ORIGIN,
         HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));

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

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

此时,就会存在一个坑爹的现象。如果 spring-cloud-gateway 不配置 cors ,则会走到

但是,如果配置了 cors,同时后端应用 response header 中也返回 Access-Control-***,意味着就会 response 中就会返回两个 Access-Control-***,导致ajax 也认为是跨域且不在允许的范围内的。

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部