動態

詳情 返回 返回

掌握 CORS 跨域請求,讀這一篇文章就夠了 - 動態 詳情

在 Web 前後端分離架構模式下,跨域(跨源)請求屬於日常的基本情況了。瀏覽器出於安全考慮,會限制 JavaScript(簡稱 JS)腳本內發起跨源 HTTP 請求,同源沒有此類限制。前端解決跨域方法有很多,比如 WebSocket 協議跨域、JSONP 請求跨域和跨域資源共享 CORS 等。

1、CORS 簡介

CORS 全稱為 Cross-Origin Resource Sharing,被譯為跨域資源共享,簡稱跨域訪問,是 W3C 制定的標準協議。它由一系列傳輸的 HTTP 標頭(首部字段)組成,瀏覽器會根據這些 HTTP 標頭決定着是否阻止前端 JS 代碼獲取跨域請求的資源。CORS 主要作用是消除各種 API 的同源限制,以便在不同源(服務器)之間共享資源,且確保跨域數據傳輸的安全性。

CORS 請求並不是一種特殊的 HTTP 請求,同樣基於 HTTP 通信協議。CORS 請求默認攜帶"origin"標頭,用於向目標網站指明請求的來源。origin 字段由三部分組成:協議、主機和端口,以下三種語法都是正確的。

origin: null
origin: <scheme>://<hostname>
origin: <scheme>://<hostname>:<port>

2、查詢瀏覽器的兼容性

推薦一個查詢瀏覽器特性、兼容性以及兼容到具體哪個版本的網站。例如查詢各瀏覽器對 CORS 的支持情況,訪問 URL 地址 https://caniuse.com/?search=CORS。如下圖所示:

Search Cors

3、同源與不同源的定義及舉例説明

同源策略是由 Netscape 提出的一個著名的安全策略,它是一種安全約定。目前,所有可支持 JS 的瀏覽器都會遵循這個策略。Ajax 是當代 Web 應用程序中獲取服務器數據的核心技術,可以實現網頁內容異步更新,Ajax 底層之 XMLHttpRequest 對象和 Fetch API 都遵循同源策略。同源策略也是瀏覽器基本的安全功能之一。

同源的定義:當兩個 URL 使用的協議、域名(主機)和端口都相同的情況下,則稱為兩個 URL 同源,反之稱兩個 URL 不同源。下表整理了同源與不同源的 URL 示例説明:

URL A URL B 結果 分析原因
https://www.example.com/a/ https://www.example.com/b/ 同源 域名相同,只有路徑不同
https://www.example.com/a/ https://www.example.com/a/c/ 同源 域名相同,只有路徑不同
http://www.example.com http://www.example.com:80 同源 80 是 HTTP 協議默認端口
https://www.example.com https://www.example.com:443 同源 443 是 HTTPS 協議默認端口
https://www.example.com http://www.example.com 不同源 域名相同,協議不同
https://www.example.com https://www.example.com:81 不同源 域名相同,端口不同
https://www.example.com https://tool.example.com 不同源 主域名相同,二級域名不同
https://www.example.com https://example.com 不同源 主域名相同,子域名不同
https://www.example.com https://39.105.183.157 不同源 域名與 IP 不同
https://www.example.com https://tool.box3.cn 不同源 完全不同的域名
http://www.example.com http://localhost 不同源 完全不同的域名

4、常見的 CORS 訪問控制場景

本例中,Nginx 服務器開啓了 HTTP/2 協議,因此在 HTTP/2 二進制編碼之前,必須將 HTTP 標頭名稱轉換為小寫。若請求頭、響應頭中包含大寫的字段名將被視為格式錯誤。

關鍵知識點:如果 CORS 跨域請求是這三種方法之一:GET、POST 或 HEAD,那麼在 HTTP 響應頭中並不需要指明 access-control-allow-methods 字段的值。

4.1 簡單請求

什麼是簡單請求?如果滿足下述所有條件,才會被認定為"簡單請求"。請注意,對於"簡單請求"瀏覽器不會發起 CORS 預檢請求。

1、HTTP 請求方法是以下三種之一:

  • GET
  • POST
  • HEAD

2、除了瀏覽器自動添加的首部字段(例如:connection,user-agent、date、referer 等)和 fetch 規範中定義的禁止使用的首部字段,以及"proxy-"和"sec-"小寫開頭的首部字段。允許設置的首部字段集合為:

  • accept
  • accept-language
  • content-language
  • content-type(見下列 3 )

3、content-type 的值是下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

4、請求中的任意 XMLHttpRequest 對象均沒有註冊任何事件監聽器;XMLHttpRequest 對象可以使用 XMLHttpRequest.upload 屬性訪問。
5、請求中沒有使用 ReadableStream 對象。

例如,請看一個 CORS 簡單請求的例子,用户訪問站點 https://tool.box3.cn,頁面嘗試跨域請求從 https://api.box3.cn 獲取數據,發起跨域請求的 JS 代碼如下所示:

const xhr = new XMLHttpRequest();
const url = 'https://api.box3.cn/example/simple';

xhr.open('GET', url);
xhr.send();

以下是瀏覽器發送給服務器的請求報文(關鍵部分信息):

:method: GET
:authority: api.box3.cn
:scheme: https
:path: /example/simple
origin: https://tool.box3.cn
user-agent: Mozilla/5.0 ... ...

以下是服務器返回的響應報文(關鍵部分信息):

:status: 200 OK
server: nginx
date: Thu, 17 Nov 2022 02:35:49 GMT
content-type: application/json; charset=utf-8
content-length: 47
access-control-allow-origin: *

本例中,服務器返回的首部字段 access-control-allow-origin: * 表明,該資源可以被任意外部域訪問接受所有的請求源

access-control-allow-origin: *

如果只希望服務器允許來自 https://www.example.com 的訪問,該首部字段的內容如下:

access-control-allow-origin: https://www.example.com

關鍵知識點:當響應的是附帶身份憑證的請求時(例如:Cookie),服務器必須明確 access-control-allow-origin 字段的值,而不能使用通配符"*",否則瀏覽器的同源策略會阻止該請求,並在控制枱拋出錯誤。

4.2 預檢請求和實際請求

首先,當請求發生跨域行為,且非簡單請求時,才會產生 CORS 預檢請求(CORS-preflight request)。其次與"簡單請求"不同的是,"預檢請求"是由瀏覽器自動發起的一個額外的 OPTIONS 請求,以獲知服務器是否授權後續的實際請求(例如:XHR 或 Fetch API 發起的 HTTP 跨域請求)。其次,OPTIONS 請求包含了兩個重要的標頭(首部字段)access-control-request-method 和 access-control-request-headers。

如下是一段需要發起 HTTP 預檢請求的 JS 代碼示例:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.box3.cn/example/request');

xhr.setRequestHeader('box3-token', '111-222-333-444');
xhr.send();

如上代碼使用 GET 請求從服務器獲取數據,該請求包含了一個自定義的請求頭(box3-token:111-222-333-444)。因為該字段名超出了"簡單請求"的定義範圍,所以瀏覽器自行判斷出這是一個非簡單請求,在"實際請求"發起之前,會先發起一個"預檢請求"。

預檢請求

下面是瀏覽器與服務器首次交互的報文信息,包括預檢請求頭和預檢響應頭(備註:user-agent 省略了部分內容):

/* 預檢請求頭 */
:method: OPTIONS
:authority: api.box3.cn
:scheme: https
:path: /example/request
access-control-request-method: GET
access-control-request-headers: box3-token
origin: https://tool.box3.cn
user-agent: Mozilla/5.0 ... ...

/* 預檢響應頭 */
:status: 204 No Content
server: nginx
date: Thu, 17 Nov 2022 02:35:35 GMT
access-control-allow-headers: box3-token
access-control-allow-origin: *

access-control-request-headers 告知服務器實際請求攜帶的自定義標頭,access-control-allow-headers 告知客户端已支持的所有自定義標頭,多個值之間以逗號分隔。

一般而言,服務器會對 OPTIONS 請求的結果添加緩存時間。目的是,客户端減少了預檢請求交互的時間,同時也減少了對服務器的壓力。比如服務器在響應頭中指定 access-control-max-age: 3600 表示該響應的有效時間為 3600 秒,也就是 1 小時。在這段時間內,瀏覽器不會對同一請求再次發起預檢請求,而是直接發起實際情況。

添加預檢請求緩存之後,本例的預檢響應頭,最新內容如下:

:status: 204 No Content
server: nginx
date: Thu, 17 Nov 2022 02:35:35 GMT
access-control-allow-headers: box3-token
access-control-allow-origin: *
access-control-max-age: 3600

關鍵知識點:對於 OPTIONS 請求,合法的 HTTP 狀態碼,應該定義在 2xx 範圍內。比如狀態碼設置為 200 或 204,都是正確的。

最後,待預檢請求通過之後,瀏覽器再發送實際請求。下面是實際請求的請求頭和響應頭:

/* 實際請求的請求頭 */
:method: GET
:authority: api.box3.cn
:scheme: https
:path: /example/request
box3-token: 111-222-333-444
origin: https://tool.box3.cn
user-agent: Mozilla/5.0 ... ...

/* 實際請求的響應頭 */
:status: 200 OK
server: nginx
date: Thu, 17 Nov 2022 02:35:35 GMT
content-type: application/json; charset=utf-8
content-length: 45
access-control-allow-origin: *
4.3 簡單請求和憑據

默認情況下,對於 XMLHttpRequest 或 Fetch API 發起的跨域請求,瀏覽器不會發送 Cookie 信息。若要攜帶 Cookie,以 XMLHttpRequest 對象為例,需要設置屬性 withCredentials 的值為 true。

本例中,站點 https://tool.box3.cn 內的 JS 腳本向 https://api.box3.cn 發起了一個簡單的 GET 跨域請求,並附帶了身份憑證 Cookie。JS 示例代碼如下:

const xhr = new XMLHttpRequest();
const url = 'https://api.box3.cn/example/simple_cookie';

xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();

下面是瀏覽器與服務器交互的報文信息之關鍵部分(備註:user-agent 省略了部分內容):

/* 簡單請求的請求頭 */
:method: GET
:authority: api.box3.cn
:path: /example/simple_cookie
:scheme: https
cookie: access-token=100;
origin: https://tool.box3.cn
user-agent: Mozilla/5.0 ... ...

/* 簡單請求的響應頭 */
:status: 200 OK
server: nginx
date: Thu, 17 Nov 2022 02:52:07 GMT
content-type: application/json; charset=utf-8
content-length: 45
access-control-allow-credentials: true
access-control-allow-origin: https://tool.box3.cn

關鍵知識點
1、服務器在響應頭中必須指定 access-control-allow-credentials: true 來表明跨域請求允許攜帶 Cookie,否則仍然會被瀏覽器的 CORS 策略阻止。
2、服務器在響應頭中必須指定 access-control-allow-origin 字段特定的域,該標頭的值不能設置為通配符 "*",否則仍然會被瀏覽器的 CORS 策略阻止。

4.4 預檢請求和憑據

首先,一個完整的 CORS 預檢請求,是由瀏覽器自動完成的,這個動作對用户是無感知的。

其次,與"簡單請求和憑據"這小節整理的 CORS 策略知識點是一致的。那意味着,在 OPTIONS 請求的響應頭中必須明確指定 access-control-allow-credentials: true 和 access-control-allow-origin 字段特定的域,否則後續的實際請求仍然會被瀏覽器的 CORS 策略阻止。

最後,在實際請求的響應頭中,也需要明確指定這兩個字段且保持與 OPTIONS 相同的值。

關鍵知識點:如果實際請求的 HTTP 方法,非 GET、POST 或 HEAD,那麼 access-control-allow-methods 字段的值不能設置為通配符"*",應設置為特定的 HTTP 請求方法名稱,多個值之間以逗號分隔。

4.5 預檢請求與重定向

回顧 4.2 小節的關鍵知識點,預檢請求指的是 OPTIONS 請求,且 HTTP 狀態碼定義在 2xx 範圍內。因此,如果一個預檢請求發生了重定向,那麼 HTTP 狀態碼一定大於 2xx,大多數瀏覽器將報告如下錯誤:

Access to XMLHttpRequest at 'https://api.box3.cn/example/request_redirect' from origin 'https://tool.box3.cn' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: Redirect is not allowed for a preflight request.

有兩種方式可以規避上述報錯行為:
1、在服務端上去掉對預檢請求的重定向。
2、將該請求優化成一個簡單請求。

5、常見的 4 種 CORS 錯誤

常見的 CORS 跨域請求錯誤,可能有以下 4 種情況(以下首部字段在服務器上配置):
1、受信來源 access-control-allow-origin 配置不正確。
2、受信的 HTTP 方法 access-control-allow-methods 配置不全。
3、受信的首部字段 access-control-allow-headers 配置不全。
4、access-control-allow-credentials 服務器與請求方之間的憑證許可配置錯誤。

6、藉助瀏覽器找錯誤

引發 CORS 錯誤的原因是跨域請求失敗導致,並非 JS 代碼層面出現的邏輯性 BUG。如果 JS 發起的 HTTP 請求產生 CORS 錯誤,在 JS 代碼層面無法獲知具體是哪裏出了問題,但是您可通過瀏覽器控制枱獲悉錯誤信息。例如在 Chrome 瀏覽器中,通過 F12 鍵啓動開發者調試工具,在 Network 面板中瞭解具體的報錯信息。如下圖所示:

瀏覽器拋出 CORS 錯誤詳情

7、認識這些 HTTP 請求頭和響應頭

7.1 HTTP 請求頭字段
Header 説明
origin 表明預檢請求或實際請求的源站。origin 的值只包括協議、域名、端口,不包含路徑和參數。
access-control-request-method 出現於預檢請求中,其作用是,通知服務器在實際請求中採用哪種 HTTP 方法。
access-control-request-headers 出現於預檢請求中,其作用是,通知服務器在實際請求中使用哪些 HTTP 請求頭。
7.2 HTTP 響應頭字段
Header 説明
access-control-allow-origin 指定請求的資源能共享給哪些域。該字段只能指定一個來源。對於不需要攜帶身份憑證的請求,可以設置為通配符 *,表示允許所有來源訪問。
access-control-expose-headers 在跨源訪問時,XMLHttpRequest 對象的 getResponseHeader() 方法只能拿到一些最基本的響應頭。如果需要獲取其他響應頭,通過該字段添加白名單。
access-control-allow-methods 對於預檢請求的響應,指明實際請求允許使用哪些 HTTP 方法。
access-control-allow-headers 對於預檢請求的響應,指明實際請求允許攜帶哪些 HTTP 頭。
access-control-max-age 指定預檢請求的有效期,單位是秒。目的是減少發起預檢請求的次數。
access-control-allow-credentials 當設置為 true 時,告訴瀏覽器將響應公開給前端 JavaScript 代碼。請注意,該值嚴格區分大小寫,正確的寫法是全小寫。

Add a new 評論

Some HTML is okay.