摘要:CORS是一個(gè)W3C標(biāo)準(zhǔn)窘问,全程是“跨域資源共享”(Cross-origin resource sharing)。CORS允許瀏覽器向跨源服務(wù)器發(fā)出XMLHttpRequest請(qǐng)求话侄,以克服AJAX只能基于同源策略的使用限制。本文將詳細(xì)介紹CORS的內(nèi)部機(jī)制。
1. 簡(jiǎn)介
??CORS需要瀏覽器與服務(wù)器同時(shí)支持杨赤。目前,所有瀏覽器都支持該功能截汪,IE瀏覽器不能低于IE10疾牲。
??整個(gè)CORS通信過程,都是瀏覽器自動(dòng)完成挫鸽,不需要用戶參與说敏。對(duì)于開發(fā)者來說,CORS通信與同源的AJAX通信沒有差別丢郊,代碼完全一樣盔沫。瀏覽器一旦發(fā)現(xiàn)AJAX請(qǐng)求跨源,就會(huì)自動(dòng)添加一些附加的頭信息枫匾,有時(shí)還會(huì)多出一次附加的請(qǐng)求(如添加了Origin : http://localhost:63343架诞,Access-Control-Request-Headers : accept, content-type,Access-Control-Request-Method : POST)干茉,但是用戶不會(huì)有感覺谴忧。
??因此,實(shí)現(xiàn)CORS通信的關(guān)鍵是服務(wù)器。只要服務(wù)器實(shí)現(xiàn)了CORS接口或者約定沾谓,就可以跨源通信委造。
2.兩種請(qǐng)求
??瀏覽器把CORS請(qǐng)求分成:簡(jiǎn)單請(qǐng)求(Simple Request)和非簡(jiǎn)單請(qǐng)求(Not-so simple request)。
??只要同時(shí)滿足一下兩個(gè)條件均驶,就屬于簡(jiǎn)單請(qǐng)求昏兆。
??(1)請(qǐng)求方法是HEAD/GET/POST三種方法之一;
??(2)HTTP的頭信息不超出一下幾種字段:Accept/Accept-Encoding/Accept-Language/Cache-Control/Connection/Cookie/Host/If-Modified-Since/Referer/User-Agent/Content-Type/Content-Language妇穴。其中Content-Type僅限于三個(gè)值:application/x-www-form-urlencoded爬虱、multipart/form-data、text/pain腾它。
??凡是不滿足上面兩個(gè)條件跑筝,就屬于非簡(jiǎn)單請(qǐng)求。瀏覽器對(duì)于兩種請(qǐng)求的處理是不一樣的瞒滴。
3. 簡(jiǎn)單請(qǐng)求
3.1 基本流程
??對(duì)于簡(jiǎn)單請(qǐng)求曲梗,瀏覽器直接發(fā)出CORS請(qǐng)求,在HTTP HEADER中增加了Origin字段逛腿。如下:
POST /nlp/segment HTTP/1.1
Host: 10.1.222.80:8084
Connection: keep-alive
Content-Length: 90
Accept: */*; q=0.01
Origin: http://localhost:63343
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Content-Type: application/ x-www-form-urlencoded;charset=UTF-8
Referer: http://localhost:63343/BZZZ/bz.html?_ijt=eu8vkf2u50ccl1gshpr4lhjt3r
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8
??上面的頭信息中稀并,Origin字段用來說明,本次請(qǐng)求來自哪個(gè)源(協(xié)議+域名+端口)单默。服務(wù)器根據(jù)這個(gè)值碘举,決定是否同意這次請(qǐng)求。
如果Origin制定的源不在許可范圍之內(nèi)搁廓,服務(wù)器會(huì)返回一個(gè)正常的HTTP回應(yīng)引颈。瀏覽器發(fā)現(xiàn)Http Response頭信息中沒有包含Access-Control-Allow-Origin字段,就判斷出錯(cuò)并拋出一個(gè)錯(cuò)誤境蜕,被XMLHttpRequest的onerror回調(diào)函數(shù)捕獲蝙场。注意,這種錯(cuò)誤無法通過狀態(tài)碼識(shí)別粱年,因?yàn)镠TTP回應(yīng)的狀態(tài)碼有可能是200.
??如果Origin制定的域名在許可范圍內(nèi)售滤,服務(wù)器返回的響應(yīng)中多出幾個(gè)頭信息字段:
Access-Control-Allow-Headers:Content-Type
Access-Control-Allow-Methods:GET,POST,PUT,OPTIONS,DELETE
Access-Control-Expose-Headers: FooBar
Access-Control-Allow-Origin:*
Content-Type:text/html; charset=UTF-8
??上面的頭信息中,有三個(gè)與CORS請(qǐng)求相關(guān)的字段台诗,均以Access-Control-開頭完箩。
(1). Access-Control-Allow-Origin
??該字段是必須的,它的值要么是請(qǐng)求時(shí)Origin字段的值拉队,要么是一個(gè)*弊知,表示接受任意域名的請(qǐng)求。
(2). Access-Control-Allow-Credentials
??該字段可選粱快,它是一個(gè)Bool值秩彤,表示是否允許發(fā)送Cookie叔扼。默認(rèn)情況下,Cookie不包括在CORS請(qǐng)求中漫雷,設(shè)為true瓜富,即表示服務(wù)器明確許可,Cookie可以包含在請(qǐng)求中珊拼,一起發(fā)給服務(wù)器食呻。這個(gè)值只能設(shè)為true流炕,沒有false取值澎现,如果服務(wù)器不要瀏覽器發(fā)送Cookie,刪除該字段即可每辟。
(3). Access-Control-Allow-Methods
??該字段可選剑辫,如果允許跨域請(qǐng)求的方法是PUT/GET/POST/OPTIONS/DELETE等其中的一項(xiàng)或幾項(xiàng),就需要設(shè)置該字段的值為”PUT,GET,POST,OPTIONS”渠欺。
(4). Access-Control-Expose-Headers
??該字段可選妹蔽,CORS請(qǐng)求時(shí),XMLHttpRequest對(duì)象的getResponseHeader()方法只能拿到6哥基本字段:Cache-Control挠将、Content-Language胳岂、Content-Type、Expires舔稀、Last-Modified乳丰、Pragma。如果想拿到其他字段内贮,就必須在Access-Control-Expose-Headers字段中指定产园。上面的例子制定,getResponseHeader(’FooBar’)可以返回FooBar字段的值夜郁。
(5). Access-Control-Allow-Headers
??該字段可選什燕,CORS請(qǐng)求時(shí),如果是自定義Http Request Header竞端,就必須在該字段中指定對(duì)應(yīng)的Header Name屎即,如例子中自定義Header為’Test’,那么需要在服務(wù)器端制定該字段為’Test’事富。
3.2 withCredentials屬性
??上面提到CORS請(qǐng)求默認(rèn)不發(fā)送Cookie和HTTP認(rèn)證信息技俐。如果要把Cookie發(fā)到服務(wù)器,一方面要服務(wù)器同意赵颅,指定Access-Control-Allow-Credentials字段虽另。
Access-Control-Allow-Credentials: true
??另外一方面,開發(fā)者必須在AJAX請(qǐng)求中打開withCredentials屬性饺谬。如下:
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
??否則捂刺,即使服務(wù)器同意發(fā)送Cookie谣拣,瀏覽器也不會(huì)發(fā)送∽逭梗或者服務(wù)器要求設(shè)置Cookie森缠,瀏覽器也不會(huì)處理。如果省略withCredentials字段設(shè)置仪缸,有些瀏覽器還是會(huì)一起發(fā)送Cookie贵涵。這時(shí)需要顯示關(guān)閉withCredentials。
xhr.withCredentials = false;
??需要注意的是恰画,如果發(fā)送Cookie宾茂,Access-Control-Allow-Origin就不能設(shè)為”*”,必須制定明確的拴还、與請(qǐng)求網(wǎng)頁一直的域名跨晴。同時(shí)Cookie依然遵循同源側(cè)策略,只有用服務(wù)器域名設(shè)置設(shè)置的Cookie才會(huì)上傳片林,其他域名的Cookie并不會(huì)上傳端盆,且(跨源)發(fā)起請(qǐng)求的網(wǎng)頁代碼中document.cookie無法讀取服務(wù)器域名下的Cookie。
4. 非簡(jiǎn)單請(qǐng)求
4.1 預(yù)檢請(qǐng)求
??非簡(jiǎn)單請(qǐng)求是對(duì)服務(wù)器有特殊要求的請(qǐng)求费封,比如請(qǐng)求方法是PUT/DELETE焕妙,或者Content-Type字段的類型是application/json。
非簡(jiǎn)單請(qǐng)求的CORS請(qǐng)求弓摘,會(huì)在正式itongxin之前焚鹊,增加愛一次HTTP查詢請(qǐng)求,叫預(yù)檢請(qǐng)求(preflight)衣盾。
??瀏覽器現(xiàn)詢問服務(wù)器寺旺,當(dāng)前網(wǎng)頁所在的域名是否在服務(wù)器的許可名單中,以及可以使用哪些HTTP動(dòng)詞和頭信息字段势决。只有得到肯定答復(fù)阻塑,瀏覽器才會(huì)發(fā)出正式的XMLHttpRequest請(qǐng)求,否則就報(bào)錯(cuò)果复。下面是一段瀏覽器的JS腳本:
var test1 = {method:"segment", format:"json", message:["我是中國人陈莽!","歡迎來到中國"]};
$.ajax({
url : "http://10.1.222.80:8084/nlp/segment",
type : "post",
dataType : "json",
contentType:"application/json;charset=utf-8",
data :JSON.stringify(test1),
crossDomain: true,
/*headers:{
"Content-Type":"application/x-www-form-urlencoded"
},*/
timeout : 10000
});
??上面的請(qǐng)求中是Content-Type為application/json的請(qǐng)求。瀏覽器認(rèn)為這是一個(gè)非簡(jiǎn)單請(qǐng)求虽抄,自動(dòng)向服務(wù)器發(fā)送一個(gè)預(yù)檢請(qǐng)求走搁,要求服務(wù)器確認(rèn)可以這樣請(qǐng)求。下面是預(yù)檢請(qǐng)求的HTTP頭信息:
OPTIONS /nlp/segment HTTP/1.1
Host: 10.1.222.80:8084
Connection: keep-alive
Access-Control-Request-Method: POST
Origin: http://localhost:63343
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/50.0.2661.102 Safari/537.36
Access-Control-Request-Headers: accept, content-type
Accept: */*
Referer: http://localhost:63343/BZZZ/bz.html?_ijt=eu8vkf2u50ccl1gshpr4lhjt3r
Accept-Encoding: gzip, deflate, sdch
Accept-Language: zh-CN,zh;q=0.8
??預(yù)檢請(qǐng)求使用的請(qǐng)求方法是OPTIONS迈窟,表示這個(gè)請(qǐng)求用來詢問私植。頭信息中關(guān)鍵字段是Origin,表示請(qǐng)求來自哪個(gè)源车酣。
除了Origin字段曲稼,預(yù)檢請(qǐng)求的頭信息還包括兩個(gè)特殊字段:
??(1). Access-Control-Request-Method
??該字段是必須的索绪,用來列出瀏覽器的CORS請(qǐng)求會(huì)用到哪些HTTP方法,例子中是POST贫悄。
??(2). Access-Control-Request-Headers
??該字段是都好分割的字符串瑞驱,指定瀏覽器CORS請(qǐng)求會(huì)額外發(fā)送的頭信息字段,上面的例子默認(rèn)application/json對(duì)應(yīng)的額外字段是”Content-Type”窄坦。
4.2 預(yù)檢請(qǐng)求的回應(yīng)Response
??服務(wù)器收到預(yù)檢請(qǐng)求后唤反,檢查Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后鸭津,確認(rèn)允許跨源請(qǐng)求彤侍,并做出回應(yīng)。如下:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Connection: close
Time-Used: 25ms
Content-Length-UnGzipped: 7
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,PUT,OPTIONS,DELETE
Access-Control-Allow-Headers: Content-Type
Content-Encoding: gzip
Content-Length: 33
??上面的Http Response中曙博,關(guān)鍵是Access-Control-Allow-Origin字段拥刻,表示http://localhost:63343可以請(qǐng)求數(shù)據(jù),該字段可以設(shè)為”*”父泳,表示同意任意跨源請(qǐng)求。
??如果服務(wù)器否定了預(yù)檢請(qǐng)求吴汪,會(huì)返回一個(gè)正常的HTTP Response惠窄,但是沒有任何CORS相關(guān)的頭信息字段。這時(shí)候?yàn)g覽器認(rèn)為i額服務(wù)器不同意預(yù)檢請(qǐng)求漾橙,因此出發(fā)一個(gè)錯(cuò)誤杆融,被XMLHttpRequest對(duì)象的onerror毀掉函數(shù)捕獲∷耍控制臺(tái)會(huì)打印出如下報(bào)錯(cuò)信息脾歇。
XMLHttpRequest cannot load http://localhost:63343.
Origin http://localhost:63343 is not allowed by Access-Control-Allow-Origin.
??服務(wù)器響應(yīng)的CORS其他相關(guān)字段如下:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,POST,PUT,OPTIONS,DELETE
Access-Control-Allow-Headers: Content-Type
??(1). Access-Control-Allow-Methods
??該字段是必需的,其取值是逗號(hào)分割的字符串淘捡,表明服務(wù)器支持的所有跨域請(qǐng)求的方法藕各。注意,返回的是所有支持的方法焦除,而不單是瀏覽器請(qǐng)求的哪個(gè)方法激况。這個(gè)是為了避免多次預(yù)檢請(qǐng)求。
??(2). Access-Control-Allow-Headers
??如果瀏覽器請(qǐng)求包括Access-Control-Allow-Headers字段膘魄,則該字段是必須的乌逐,其取值是一個(gè)逗號(hào)分割的字符串,表明服務(wù)器支持的所有頭信息字段创葡,不限于瀏覽器在預(yù)檢中請(qǐng)求的字段浙踢。
??(3). Access-Control-Allow-Credentials
??該字段與簡(jiǎn)單請(qǐng)求時(shí)字段含義相同。
??(4). Access-Control-Max-Age
??該字段可選灿渴,用來制定本次預(yù)檢請(qǐng)求的有效期洛波,單位為秒呐芥。緩存于瀏覽器中,在有效期中奋岁,不需要發(fā)出另一條預(yù)檢請(qǐng)求思瘟。
4.3 瀏覽器的正常請(qǐng)求和回應(yīng)
??一旦服務(wù)器通過了預(yù)檢請(qǐng)求,以后每次瀏覽器正常的CORS請(qǐng)求就都與簡(jiǎn)單請(qǐng)求一樣闻伶,包括Origin字段信息滨攻。服務(wù)器的回應(yīng)也會(huì)有Access-Control-Allow-Origin字段。
5. 與JSONP比較
??CORS與Jsonp使用目的相同蓝翰,但是Jsonp只支持GET請(qǐng)求光绕,CORS支持所有類型的HTTP請(qǐng)求。JSONP的有時(shí)在于支持老式瀏覽器畜份,以及向不支持CORS的網(wǎng)站請(qǐng)求數(shù)據(jù)诞帐。
黃博博
2016年8月2日