隨著Web開放的程度越來越高,透過瀏覽器跨域獲取資源的需求已經變得非常普遍。在我看來,如果Web API不能針對瀏覽器提供跨域資源共享的能力,它甚至就不應該被稱為Web API。從另一方面來看,瀏覽器作為進入Internet最大的入口,是各大IT公司的必爭之地,所以瀏覽器市場出現了種類繁多、魚龍混雜的局面。針對這兩點,我們迫切需要一種能夠被各個瀏覽器廠商共同遵循的標準來對跨域資源共享作出規範,這就是由W3C指定2的CORS(Cross-Origin Resource Sharing)規範。
目錄
CORS是如何工作的?
對響應報頭的授權
預檢機制
是否支援使用者憑證
一、CORS是如何工作的?
基於Web的資源共享涉及到兩個基本的角色,即資源的提供者和消費者。針對我們前面演示的應用場景,即顯示在瀏覽器中的某個Web頁面透過呼叫Web API的方式來獲取它所需的資源,資源提供者為Web API本身,透過傳送Ajax請求來呼叫Web API的JavaScript程式為資源的消費者。
CORS旨在定義一種規範讓瀏覽器在接收到從提供者獲取的資源時能夠正決定是否應該將此資源分發給消費者作進一步處理。CROS利用資源提供者的顯式授權來決定目標資源是否應該與消費者共享。換句話說,瀏覽器需要得到提供者的授權之後才會將其提供的資源分發給消費者。那麼,資源的提供者如何進行資源的授權,並將授權的結果告訴瀏覽器呢?
具體的實現其實很簡單。如果瀏覽器 自身提供對CROS的支援,由它傳送的請求會攜帶一個名為“Origin”的報頭表明請求頁面所在的站點。對於前面我們演示例項中呼叫Web API獲取聯絡人列表的請求來說,它就具有如下一個“Origin”報頭。
1: Origin: http://localhost:9527
資源獲取請求被提供者接收之後,它可以根據該報頭確定提供的資源需要共享給誰。資源提供者的授權透過一個名為“Access-Control-Allow-Origin”的響應報頭來承載,其報頭值表示得到授權的站點。一般來說,如果資源的提供者認可了當前請求的“Origin”報頭攜帶的站點,那麼它會將該站點作為“Access-Control-Allow-Origin”響應報頭的值。
除了指定具體的源並對其作針對性授權之外,資源提供者還可以將“Access-Control-Allow-Origin”報頭值設定為“*”對所有消費者授權。換言之,如果作了這樣的設定,意味著由其提供的是一種公共資源,所以在做此設定之前需要慎重。如果針對請求著的授權不被允許,資源提供者可以將此響應報頭值設定為“null”,或者讓響應不具有此報頭。
當瀏覽器接收到包含資源的響應之後,會提取此“Access-Control-Allow-Origin”響應報頭的值。如果此值為“*”或者包含的源列表包含此前請求的源(即請求的“Origin”報頭值),意味著資源的消費者獲取了提供者獲取和操作資源的許可權,所以瀏覽器會允許JavaScript程式操作獲取的資源。如果此響應報頭不存在或者其值為“null”,客戶端JavaScript程式針對資源的操作會被拒絕。
二、對響應報頭的授權
資源提供者除了透過設定“Access-Control-Allow-Origin”報頭對提供的主體資源進行授權之外,還可以透過設定另一個名為“Access-Control-Expose-Headers”的報頭對響應報頭進行授權。具體來說,此“Access-Control-Expose-Headers”的報頭用於設定一組直接暴露給客戶端JavaScript程式的響應報頭,沒有在此列表的響應報頭 對於客戶端JavaScript程式是不可見的。
但是由此實現的針對響應報頭的授權針對簡單響應報頭是無效的,客戶端JavaScript程式總是具有獲取它們的許可權。對於CORS規範來說,這裡所謂的“簡單響應報頭(Simple Response Header)”包含如下6種。
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
我們知道用於實現Ajax請求的XMLHttpRequest具有一個getResponseHeader方法,呼叫它會返回一組響應報頭的列表。按照這裡介紹的針對響應報頭的授權原則,只有在“Access-Control-Expose-Headers”報頭中指定的報頭和簡單響應報頭才會包含在該方法返回的列表中。
三、預檢機制
W3C的CORS規範將跨域資源請求劃分為兩種型別,一種被稱為“簡單請求(Simple Request)”。要弄清楚CORS規範將那些型別的跨域資源請求劃分為簡單請求的範疇,需要額外瞭解幾個名稱的含義,其中包括“簡單(HTTP)方法(Simple Method)”、“簡單(請求)報頭(Simple Header)”和“自定義請求報頭(Author Request Header/Custom Request Header)”。
CORS規範將GET、HEAD和POST這三個HTTP方法視為“簡單HTTP方法”,而將請求報頭Accept, Accept-Language, Content-Language以及採用如下三種媒體型別的報頭Content-Type稱為“簡單請求報頭”
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
請求的報頭包含兩種型別,一類是透過瀏覽器自動生成的報頭,另一種則是由JavaScript程式自行新增的報頭(比如呼叫XMLHttpRequest的setRequestHeader方法可以為生成的Ajax請求新增任意報頭),後者被稱為“自定義報頭”。
CORS規範將服務如下條件的跨域資源請求劃分為簡單請求:請求採用簡單HTTP方法,並且其自定義請求報頭空或者所有自定義請求報頭均為簡單請求報頭。之所以作如此劃分是因為具有這些特性的請求不是以更新(新增、修改和刪除)資源為目的,服務端對請求的處理不會導致自身維護資源的改變。
對於簡單跨域資源請求來說,瀏覽器將兩個步驟(取得授權和獲取資源)合二為一,由於不涉及到資源的改變,所以不會帶來任何副作用(Side Effect)。如果針對請求的處理過程會涉及到對資源的改變,這樣做就會有問題了。按照CORS規範的規定,瀏覽器應該採用一種被稱為“預檢(Preflight)”的機制來完成非簡單跨域資源請求。
所謂預檢機制就是說瀏覽器在傳送真正的跨域資源請求前,先傳送一個預檢請求(Preflight Request)。預檢請求為一個採用HTTP-OPTIONS方法的請求,這是一個不包含主體的請求,同時使用者憑證相關的報頭也會被剔除。基於真正資源請求的一些輔助授權的資訊會包含在此預檢請求的相應報頭中。除了代表請求頁面所在站點的“Origin”報頭之外,如下所示的是兩個典型的請求報頭。
- Access-Control-Request-Method:真正跨域資源請求採用的HTTP方法。
- Access-Control-Request-Headers:真正跨域資源請求攜帶的自定義報頭列表。
資源的提供者在接收到預檢請求之後,根據其提供的相關報頭進行授權檢驗,具體的檢驗邏輯即包括確定請求站點是否值得信任,以及請求採用HTTP方法和自定義報頭是否被允許。如果預檢請求沒有透過授權檢驗,資源提供者一般會返回一個狀態為“400, Bad Reuqest”的響應。反之則會返回一個狀態為“200, OK”的響應,授權相關資訊會包含在響應報頭中。除了上面介紹的“Access-Control-Allow-Origin”和“Access-Control-Expose-Headers”報頭之外,預檢請求的響應還具有如下3個典型的報頭。
- Access-Control-Allow-Methods:跨域資源請求允許採用的HTTP方法列表。
- Access-Control-Allow-Headers:跨域資源請求允許攜帶的自定義報頭列表。
- Access-Control-Max-Age:瀏覽器可以將響應結果進行快取的時間(單位為秒),這樣可以讓瀏覽器避免頻繁地傳送預檢請求。
瀏覽器在接收到預檢響應之後,會根據響應報頭確定後續傳送的真正跨域資源請求是否會被接受,相關的檢驗包括針對服務端允許站點以及HTTP方法和自定義請求報頭(利用響應報頭“Access-Control-Allow-Methods”和“Access-Control-Allow-Headers”)的檢驗。具體的檢驗邏輯如下
- 透過請求的“Origin”報頭表示的源站點必須存在於“Access-Control-Allow-Origin”響應報頭標識的站點列表中。
- 響應報頭“Access-Control-Allow-Methods”不存在,或者預檢請求的“Access-Control-Request-Method”報頭表示的請求方法在其列表之內。
- 預檢請求的“Access-Control-Request-Headers”報頭儲存的報頭名稱均在響應報頭“Access-Control-Allow-Headers”表示的報頭列表之內。
只有在確定服務端一定會接受的情況下,瀏覽器才會傳送真正跨域資源請求。預檢響應結果會被瀏覽器快取,在“Access-Control-Max-Age”報頭設定的時間內,快取的結果將被瀏覽器使用者進行授權檢驗,所以在此期間不會再有預檢請求傳送。
四、是否支援使用者憑證
在預設情況下,利用XMLHttpReuqest傳送的Ajax請求不會攜帶使用者憑證相關的敏感資訊,這裡的使用者憑證型別包括Cookie、HTTP-Authentication報頭以及客戶端X.509證照(採用支援客戶端證照的TLS/SSL)等。如果需要使用者憑證附加到Ajax請求上,需要將XMLHttpReuqest的withCredentials 屬性設定為True。
對於CORS來說,是否支援使用者憑證也是授權檢驗的一個環節。換句話說,只有在服務端顯式支援使用者憑證的情況下,攜帶了使用者憑證的請求才會被認為是有效的。在W3C的CORS規範來說,服務端利用響應報頭“Access-Control-Allow-Credentials”來表明自身是否支援使用者憑證。
也就是說,如果客戶客戶端JavaScript程式利用一個withCredentials屬性為true的XMLHttpReuqest傳送了一個跨域資源請求,但是瀏覽器得到的響應中不具有一個值為“true”的響應報頭“Access-Control-Allow-Credentials”,它對獲取資源的操作將會瀏覽器拒絕。
上面我們對W3C的CORS規範作了概括性的介紹,由於篇幅所限,很多的細節並沒有涉及。如果讀者朋友們對此有興趣,我個人強烈推薦直接閱讀W3C的官方文件。由於官方文件的文字描述較為“生硬”,可能需要多讀幾遍才能將資源提供者和瀏覽器如何處理資源授權流程搞清楚。
CORS系列文章[1] 同源策略與JSONP
[2] 利用擴充套件讓ASP.NET Web API支援JSONP
[3] W3C的CORS規範
[4] 利用擴充套件讓ASP.NET Web API支援CORS
[5] ASP.NET Web API自身對CORS的支援: 從例項開始
[6] ASP.NET Web API自身對CORS的支援: CORS授權策略的定義和提供
[7] ASP.NET Web API自身對CORS的支援: CORS授權檢驗的實施
[8] ASP.NET Web API自身對CORS的支援: CorsMessageHandler