大數據

跨域請求出現preflight request失敗的問題的解決

問題出現

這兩天在項目聯調過程中突然前端同學報告出現CORS跨域問題無法訪問。剛聽到很奇怪,因為已經在項目裡面設置了CORS規則,理論上不會出現這個問題。

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String orignalHeader = request.getHeader("Origin");

        if (orignalHeader != null ) {
            Matcher m = CORS_ALLOW_ORIGIN_REGEX.matcher(orignalHeader);
            if (m.matches()) {
                response.addHeader("Access-Control-Allow-Origin", orignalHeader);
                response.addHeader("Access-Control-Allow-Credentials", "true");
                response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
                response.addHeader("Access-Control-Allow-Headers", "x-dataplus-csrf, Content-Type");
            }
        }
    }

拿到前端給的錯誤提示後發現了一個奇怪的問題,提示Response to preflight request doesn't pass access control check中的preflight request是什麼?

image.png

Preflight request介紹

瞭解得知跨域資源共享標準新增了一組 HTTP 首部字段,允許服務器聲明哪些源站通過瀏覽器有權限訪問哪些資源。同時規範要求,對那些可能對服務器數據產生副作用的 HTTP 請求方法(特別是 GET 以外的 HTTP 請求,或者搭配某些 MIME 類型的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求(preflight request),從而獲知服務端是否允許該跨域請求。服務器確認允許之後,才發起實際的 HTTP 請求。在預檢請求的返回中,服務器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 和 HTTP 認證相關數據)。

一個Preflight request的流程可以如下圖所示

image.png

什麼樣的請求會產生Preflight request呢?當請求滿足下述任一條件時,即應首先發送Preflight request請求:

  • 使用了下面任一 HTTP 方法:

    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 人為設置了對 CORS 安全的首部字段集合之外的其他首部字段。該集合為:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type (需要注意額外的限制)
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 的值不屬於下列之一:

    • application/x-www-form-urlencoded
    • multipart/form-data
    • text/plain
  • 請求中的XMLHttpRequestUpload 對象註冊了任意多個事件監聽器。
  • 請求中使用了ReadableStream對象。

在我們的例子中正是使用了POST方法傳遞了一個Content-Type為application/json的數據到後端。而這個OPTION請求返回失敗後瀏覽器並沒有繼續下發POST請求。

解決方案

在弄清楚問題後,我們瞭解只要給Preflight request優先通過就可以引導後續請求繼續下發。對此,我們改造CORS Filter來解決這個問題。

  • 首先對OPTION請求放入HTTP 200的響應內容。
  • 對於Preflight request詢問中的的Access-Control-Request-Headers予以通過
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String orignalHeader = request.getHeader("Origin");

        if (orignalHeader != null ) {
            Matcher m = CORS_ALLOW_ORIGIN_REGEX.matcher(orignalHeader);
            if (m.matches()) {
                response.addHeader("Access-Control-Allow-Origin", orignalHeader);
                response.addHeader("Access-Control-Allow-Credentials", "true");
                response.addHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
                response.addHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
            }
        }

        if ("OPTIONS".equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            filterChain.doFilter(request, response);
        }
    }

注意事項

但是天不遂人願,在上述改造後理論上應該是可以解決Preflight request問題,可以測試發現依然有問題。這時我們注意到錯誤信息中提到的另外一句Redirect is not allowed for a preflight request.

為什麼會有Redirect事情發生呢,原來所有請求在進入我們的CORS Filter之前,會首先通過SSO Filter做登錄檢測。而這個Preflight request並沒有攜帶登錄信息,導致OPTION請求被跳轉到了登錄頁面。同理如果引用了Spring Security組件的的話也會出現首先被登錄驗證給過濾的問題。

找到問題就比較好辦了,調整CORS Filter優先級,讓其先於登錄驗證進行就好了。對此我們調整registrationBean的order從默認的Integer.MAX_VALUE到1就好了。

    @Bean(name = "corsFilter")
    public FilterRegistrationBean corsFilter() {
        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
        registrationBean.setFilter(corsFilterBean());
        registrationBean.setUrlPatterns(Lists.newArrayList("/*"));
        registrationBean.setOrder(1);
        return registrationBean;
    }

Leave a Reply

Your email address will not be published. Required fields are marked *