問題出現
這兩天在項目聯調過程中突然前端同學報告出現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是什麼?
Preflight request介紹
瞭解得知跨域資源共享標準新增了一組 HTTP 首部字段,允許服務器聲明哪些源站通過瀏覽器有權限訪問哪些資源。同時規範要求,對那些可能對服務器數據產生副作用的 HTTP 請求方法(特別是 GET 以外的 HTTP 請求,或者搭配某些 MIME 類型的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求(preflight request),從而獲知服務端是否允許該跨域請求。服務器確認允許之後,才發起實際的 HTTP 請求。在預檢請求的返回中,服務器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 和 HTTP 認證相關數據)。
一個Preflight request的流程可以如下圖所示
什麼樣的請求會產生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;
}