1. 程式人生 > >跨域二三事之options請求

跨域二三事之options請求

背景

今天小夥伴問了一個比較奇怪的問題 在某個子系統中通過跨域請求發現POST請求無法正常拿到cookie導致報錯

我們從程式碼來分析一下原因

思路

最重要的永遠是碰到問題解決問題的思路。首先碰到xhr沒有攜帶cookie導致重定向到cas登入頁的問題之前碰到過

跨域二三事之withCredentials

那麼小夥伴也確定是加了對應的withCredentials 並且get請求可以正常返回。

那麼緣何出現了POST請求不成功呢?

知識點

我們知道使用cors做跨域請求是分兩種的。

  • 簡單請求
  • 非簡單請求

The term user credentials for the purposes of this specification means cookies, HTTP authentication, and client-side SSL certificates that would be sent based on the user agent’s previous interactions with the origin. Specifically it does not refer to proxy authentication or the Origin

header. [COOKIES] The term cross-origin is used to mean non same origin. A method is said to be a simple method if it is a case-sensitive match for one of the following:

GET

HEAD

POST

To protect resources against cross-origin requests that could not originate from certain user agents before this specification existed a

preflight request is made to ensure that the resource is aware of this specification. The result of this request is stored in a preflight result cache. The steps below describe what user agents must do for a cross-origin request with preflight. This is a request to a non same-origin URL that first needs to be authorized using either a
preflight result cache
entry or a preflight request.

Go to the next step if the following conditions are true:

For request method there either is a method cache match or it is a simple method and the force preflight flag is unset.

For every header of author request headers there either is a header cache match for the field name or it is a simple header.

Otherwise, make a preflight request. Fetch the request URL from originsource origin using referrer source as override referrer source with the manual redirect flag and the block cookies flag set, using the method OPTIONS, and with the following additional constraints:

Include an Access-Control-Request-Method header with as header field value the request method (even when that is a simple method).

If author request headers is not empty include an Access-Control-Request-Headers header with as header field value a comma-separated list of the header field names from author request headers in lexicographical order, each converted to ASCII lowercase (even when one or more are a simple header).

Exclude the author request headers.

Exclude user credentials.

Exclude the request entity body.

這裡的preflight也就是我們經常說的options請求【需要注意簡單請求的命中是大小寫敏感的】

但是options具有一個特點在 傳送時 會排除author header,user credentials,request body

這樣自然不會發送cookie

解析

之前的方案在做跨域並未使用filter做【filter的生命週期各位學習servlet應該很熟悉】而是採用的Spring interceptor

我們在配置跨域的時候springboot提供了較好的方案

@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**");
}

spring的cors使用了

/*
 * Copyright 2002-2016 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
package org.springframework.web.cors;
 
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
 
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.WebUtils;
 
/**
 * The default implementation of {@link CorsProcessor}, as defined by the
 * <a href="http://www.w3.org/TR/cors/">CORS W3C recommendation</a>.
 *
 * <p>Note that when input {@link CorsConfiguration} is {@code null}, this
 * implementation does not reject simple or actual requests outright but simply
 * avoid adding CORS headers to the response. CORS processing is also skipped
 * if the response already contains CORS headers, or if the request is detected
 * as a same-origin one.
 *
 * @author Sebastien Deleuze
 * @author Rossen Stoyanchev
 * @since 4.2
 */
public class DefaultCorsProcessor implements CorsProcessor {
 
   private static final Charset UTF8_CHARSET = Charset.forName("UTF-8");
 
   private static final Log logger = LogFactory.getLog(DefaultCorsProcessor.class);
 
 
   @Override
   @SuppressWarnings("resource")
   public boolean processRequest(CorsConfiguration config, HttpServletRequest request, HttpServletResponse response)
         throws IOException {
 
      if (!CorsUtils.isCorsRequest(request)) {
         return true;
      }
 
      ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
      if (responseHasCors(serverResponse)) {
         logger.debug("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header");
         return true;
      }
 
      ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
      if (WebUtils.isSameOrigin(serverRequest)) {
         logger.debug("Skip CORS processing: request is from same origin");
         return true;
      }
 
      boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
      if (config == null) {
         if (preFlightRequest) {
            rejectRequest(serverResponse);
            return false;
         }
         else {
            return true;
         }
      }
 
      return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
   }
 
   private boolean responseHasCors(ServerHttpResponse response) {
      try {
         return (response.getHeaders().getAccessControlAllowOrigin() != null);
      }
      catch (NullPointerException npe) {
         // SPR-11919 and https://issues.jboss.org/browse/WFLY-3474
         return false;
      }
   }
 
   /**
    * Invoked when one of the CORS checks failed.
    * The default implementation sets the response status to 403 and writes
    * "Invalid CORS request" to the response.
    */
   protected void rejectRequest(ServerHttpResponse response) throws IOException {
      response.setStatusCode(HttpStatus.FORBIDDEN);
      response.getBody().write("Invalid CORS request".getBytes(UTF8_CHARSET));
   }
 
   /**
    * Handle the given request.
    */
   protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
         CorsConfiguration config, boolean preFlightRequest) throws IOException {
 
      String requestOrigin = request.getHeaders().getOrigin();
      String allowOrigin = checkOrigin(config, requestOrigin);
 
      HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
      List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
 
      List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
      List<String> allowHeaders = checkHeaders(config, requestHeaders);
 
      if (allowOrigin == null || allowMethods == null || (preFlightRequest && allowHeaders == null)) {
         rejectRequest(response);
         return false;
      }
 
      HttpHeaders responseHeaders = response.getHeaders();
      responseHeaders.setAccessControlAllowOrigin(allowOrigin);
      responseHeaders.add(HttpHeaders.VARY, HttpHeaders.ORIGIN);
 
      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());
      }
 
      response.flush();
      return true;
   }
 
   /**
    * Check the origin and determine the origin for the response. The default
    * implementation simply delegates to
    * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
    */
   protected String checkOrigin(CorsConfiguration config, String requestOrigin) {
      return config.checkOrigin(requestOrigin);
   }
 
   /**
    * Check the HTTP method and determine the methods for the response of a
    * pre-flight request. The default implementation simply delegates to
    * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
    */
   protected List<HttpMethod> checkMethods(CorsConfiguration config, HttpMethod requestMethod) {
      return config.checkHttpMethod(requestMethod);
   }
 
   private HttpMethod getMethodToUse(ServerHttpRequest request, boolean isPreFlight) {
      return (isPreFlight ? request.getHeaders().getAccessControlRequestMethod() : request.getMethod());
   }
 
   /**
    * Check the headers and determine the headers for the response of a
    * pre-flight request. The default implementation simply delegates to
    * {@link org.springframework.web.cors.CorsConfiguration#checkOrigin(String)}.
    */
   protected List<String> checkHeaders(CorsConfiguration config, List<String> requestHeaders) {
      return config.checkHeaders(requestHeaders);
   }
 
   private List<String> getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) {
      HttpHeaders headers = request.getHeaders();
      return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<String>(headers.keySet()));
   }
 
}
/**
 * Utility class for CORS request handling based on the
 * <a href="http://www.w3.org/TR/cors/">CORS W3C recommandation</a>.
 *
 * @author Sebastien Deleuze
 * @since 4.2
 */
public abstract class CorsUtils {
 
   /**
    * Returns {@code true} if the request is a valid CORS one.
    */
   public static boolean isCorsRequest(HttpServletRequest request) {
      return (request.getHeader(HttpHeaders.ORIGIN) != null);
   }
 
   /**
    * Returns {@code true} if the request is a valid CORS pre-flight one.
    */
   public static boolean isPreFlightRequest(HttpServletRequest request) {
      return (isCorsRequest(request) && HttpMethod.OPTIONS.matches(request.getMethod()) &&
            request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
   }
 
}

看到在PreFlightRequest的請求時會返回response

但是當我們使用casFilter的時候 由於沒有幫我們處理Options請求 在收到Options請求的時候已經判斷cookie沒有攜帶 自然導致重定向到cas畫面

也可以參考 https://stackoverflow.com/questions/10133497/cors-withcredentials-xhr-preflight-not-posting-cookies-in-firefox