理解 CORS (Cross-Origin Resource Sharing)

存在便是合理

首先理解一下「跨站HTTP請求」html

跨站HTTP(Cross-Site HTTP Reuqest)請求,是指發起請求的資源所在域不一樣於該請求所指向資源所在域的HTTP請求。
好比說,域名A(http://domaina.example)的某 Web 應用程序中經過<img>標籤引入了域名B(http://domainb.foo)站點的某圖片資源(http://domainb.foo/image.jpg),域名A的那 Web 應用就會致使瀏覽器發起一個跨站 HTTP 請求。
在如今的Web開發中,使用跨站HTTP請求加載各種資源(包括CSS、圖片、JavaScript腳本以及其餘類資源),已經成爲了一種廣泛且流行的方式。後端

出於安全考慮,瀏覽器會限制腳本中發起的跨站請求跨域

好比,使用 XMLHttpRequest 對象發起 HTTP 請求就必須遵照同源策略。 具體而言,Web 應用程序能且只能使用 XMLHttpRequest 對象向其加載的源域名發起 HTTP 請求,而不能向任何其它域名發起請求。
要注意的是,跨域並不是瀏覽器限制了發起跨站請求,而是跨站請求能夠正常發起,可是返回結果被瀏覽器攔截了。最好的例子是CSRF跨站攻擊原理,請求是發送到了後端服務器不管是否跨域。瀏覽器

理解同源策略

Cross-Site Sharing Standard 是W3C推薦的一種機制,讓Web應用服務器能支持跨站訪問控制,從而使得安全地進行跨站數據傳輸成爲可能。下面截取其中「Syntax」章節,來講明一下請求與響應的規範。緩存

For Response安全

Access-Control-Allow-Origin Response Header

該請求頭表示所請求的資源是否接受來自指定origin(根據 Origin Request Header)的請求.服務器

Access-Control-Allow-Origin: <origin> | *

舉個栗子,容許來自 http://baidu.com 的請求,你能夠這樣指定:cookie

Access-Control-Allow-Origin: http://baidu.com
Access-Control-Allow-Credentials Response Heade

告知客戶端,當請求的credientials屬性是true的時候,響應是否能夠被獲得.當它做爲預請求的響應的一部分時,它用來告知實際的請求是否使用了credentials.注意,簡單的GET請求不會預檢,因此若是一個請求是爲了獲得一個帶有credentials的資源,而響應裏又沒有Access-Control-Allow-Credentials頭信息,那麼說明這個響應被忽略了.app

Access-Control-Allow-Credentials: true | false
Access-Control-Expose-Headers Response Header

設置瀏覽器容許訪問的服務器響應請求的頭信息的白名單:cors

Access-Control-Expose-Headers: X-A-Custom-Header, X-B-Custom-Header

這樣,瀏覽器就能夠獲得服務器響應請求的頭信息中的 X-A-Custom-Header X-B-Custom-Header

Access-Control-Max-Age Response Header

這個頭告訴咱們此次預請求的結果的有效期是多久:

Access-Control-Max-Age: <delta-seconds>

delta-seconds 參數表示,容許這個預請求的參數緩存的秒數,在此期間,不用發出另外一條預檢請求.

Access-Control-Allow-Methods Response Header

指明資源能夠被請求的方式有哪些(一個或者多個). 這個響應頭信息在客戶端發出預檢請求的時候會被返回.

Access-Control-Allow-Methods: <method>[, <method>]*
Access-Control-Allow-Headers Response Header

也是在響應預檢請求的時候使用.用來指明在實際的請求中,可使用哪些自定義HTTP請求頭.好比

Access-Control-Allow-Headers: X-Custom-Header

這樣在實際的請求裏,請求頭信息裏就能夠有這麼一條:

X-Custom-Header: hello world

For Request

Origin Request Header

代表發送請求或者預請求的域

Origin: <origin>

參數origin是一個URI,告訴服務器端,請求來自哪裏.它不包含任何路徑信息,只是服務器名.

Access-Control-Request-Method Request Header

在發出預檢請求時帶有這個頭信息,告訴服務器在實際請求時會使用的請求方式

Access-Control-Request-Method: <method>
Access-Control-Request-Headers

在發出預檢請求時帶有這個頭信息,告訴服務器在實際請求時會攜帶的自定義頭信息.若有多個,能夠用逗號分開.

Access-Control-Request-Headers: <field-name>[, <field-name>]*

舉三個栗子

一個簡單的請求

好比說,假如站點 http://foo.example 的網頁應用想要訪問 http://bar.other 的資源。如下的 JavaScript 代碼應該會在 foo.example 上執行:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/public-data/';
   
function callOtherDomain() {
  if(invocation) {    
    invocation.open('GET', url, true);
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }
}

讓咱們看看,在這個場景中,瀏覽器會發送什麼的請求到服務器,而服務器又會返回什麼給瀏覽器:

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/access-control/simpleXSInvocation.html
Origin: http://foo.example


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61 
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[XML Data]

第 1~10 行是 瀏覽器 發出的請求頭。注意看第10行的請求頭 Origin,它代表了該請求來自於 http://foo.exmaple

第 13~22 行則是 http://bar.other 服務器的響應。如第16行所示,服務器返回了響應頭 Access-Control-Allow-Origin: *,這代表服務器接受來自任何站點的跨站請求。若是服務器端僅容許來自 http://foo.example 的跨站請求,它能夠返回:

Access-Control-Allow-Origin: http://foo.example

如今,除了 http://foo.example,其它站點就不能跨站訪問 http://bar.other 的資源了。

如上,經過使用 Origin 和 Access-Control-Allow-Origin 就能夠完成最簡單的跨站請求。不過 Access-Control-Allow-Origin 須要爲 * 或者包含由 Origin 指明的站點。

預請求 Prefilght

不一樣於上面討論的簡單請求,「預請求」要求必須先發送一個 OPTIONS 請求給目的站點,來查明這個跨站請求對於目的站點是否是安全可接受的。這樣作,是由於跨站請求可能會對目的站點的數據形成破壞。 當請求具有如下條件,就會被當成預請求處理:

  • 請求以 GET, HEAD 或者 POST 之外的方法發起請求。或者,使用 POST,但請求數據爲 application/x-www-form-urlencoded, multipart/form-data 或者 text/plain 之外的數據類型。好比說,用 POST 發送數據類型爲 application/xml 或者 text/xml 的 XML 數據的請求。

  • 使用自定義請求頭(好比添加諸如 X-PINGOTHER

如示例:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/post-here/';
var body = '{C}{C}{C}{C}{C}{C}{C}{C}{C}{C}Arun';
    
function callOtherDomain(){
  if(invocation)
    {
      invocation.open('POST', url, true);
      invocation.setRequestHeader('X-PINGOTHER', 'pingpong');
      invocation.setRequestHeader('Content-Type', 'application/xml');
      invocation.onreadystatechange = handler;
      invocation.send(body); 
    }

......

如上,以 XMLHttpRequest 建立了一個 POST 請求,爲該請求添加了一個自定義請求頭(X-PINGOTHER: pingpong),並指定數據類型爲 application/xml。因此,該請求是一個「預請求」形式的跨站請求。

讓咱們看看服務器與瀏覽器之間具體的交互過程:

OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER
Access-Control-Max-Age: 1728000
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

POST /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: http://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: http://foo.example
Pragma: no-cache
Cache-Control: no-cache

Arun


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:40 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

[Some GZIP'd payload]

第1至12行,使用一個 OPTIONS 發送了一個「預請求」。瀏覽器 根據請求參數,決定須要發送一個「預請求」,來探明服務器端是否接受後續真正的請求。 OPTIONS 是 HTTP/1.1 裏的方法,用來獲取更多服務器端的信息,是一個不該該對服務器數據形成影響的方法。 隨同 OPTIONS 請求,如下兩個請求頭一塊兒被髮送:

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER

請求頭Access-Control-Request-Method能夠提醒服務器跨站請求將使用POST方法,而請求頭Access-Control-Request-Headers則告知服務器該跨站請求將攜帶一個自定義請求頭X-PINGOTHER。這樣,服務器就能夠決定,在當前狀況下,是否接受該跨站請求訪問。

第15至27行是服務器的響應。該響應代表,服務器接受了客服端的跨站請求。具體能夠看看第18至21行:

Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER
Access-Control-Max-Age: 1728000

響應頭Access-Control-Allow-Methods代表服務器能夠接受POST, GET和 OPTIONS的請求方法。請注意,這個響應頭相似於HTTP/1.1 Allow: response header,但僅限於訪問控制的場景下。而響應頭Access-Control-Allow-Headers則表示服務器接受自定義請求頭X-PINGOTHER。就像Access-Control-Allow-Methods同樣,Access-Control-Allow-Headers容許以逗號分隔,傳遞一個可接受的自定義請求頭列表。最後,響應頭Access-Control-Max-Age告訴瀏覽器,本次「預請求」的響應結果有效時間是多久。在上面的例子裏,1728000秒錶明着20天內,瀏覽器在處理針對該服務器的跨站請求,均可以無需再發送「預請求」,只需根據本次結果進行判斷處理。

附帶憑證信息的請求

XMLHttpRequest和訪問控制功能,最有趣的特性就是,發送憑證請求(HTTP Cookies和驗證信息)的功能。通常而言,對於跨站請求,瀏覽器是不會發送憑證信息的。但若是將XMLHttpRequest的一個特殊標誌位設置爲true,瀏覽器就將容許該請求的發送。

http://foo.example站點的腳本向http://bar.other站點發送一個GET請求,並設置了一個Cookies值。腳本代碼以下:

var invocation = new XMLHttpRequest();
var url = 'http://bar.other/resources/credentialed-content/';
    
function callOtherDomain(){
  if(invocation) {
    invocation.open('GET', url, true);
    invocation.withCredentials = true;
    invocation.onreadystatechange = handler;
    invocation.send(); 
  }

如你所見,第七行代碼將XMLHttpRequest的withCredentials標誌設置爲true,從而使得Cookies能夠隨着請求發送。由於這是一個簡單的GET請求,因此瀏覽器不會發送一個「預請求」。可是,若是服務器端的響應中,若是沒有返回Access-Control-Allow-Credentials: true的響應頭,那麼瀏覽器將不會把響應結果傳遞給發出請求的腳本程序,以保證信息的安全。

客服端與服務器端交互示例以下:

GET /resources/access-control-with-credentials/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US; rv:1.9.1b3pre) Gecko/20081130 Minefield/3.1b3pre
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Connection: keep-alive
Referer: http://foo.example/examples/credential.html
Origin: http://foo.example
Cookie: pageAccess=2


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:34:52 GMT
Server: Apache/2.0.61 (Unix) PHP/4.4.7 mod_ssl/2.0.61 OpenSSL/0.9.7e mod_fastcgi/2.4.2 DAV/2 SVN/1.4.2
X-Powered-By: PHP/5.2.6
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 106
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain


[text/plain payload]

雖然第11行指定了要提交到http://bar.other的內容的Cookie信息,可是若是bar.other的響應頭裏沒有Access-Control-Allow-Credentials:true(第19行),則響應會被忽略. 特別注意: 給一個帶有withCredentials的請求發送響應的時候,服務器端必須指定容許請求的域名,不能使用''.上面這個例子中,若是響應頭是這樣的:Access-Control-Allow-Origin: ,則響應會失敗. 在這個例子裏,由於Access-Control-Allow-Origin的值是http://foo.example這個指定的請求域名,因此客戶端把帶有憑證信息的內容被返回給了客戶端. 另外注意第22行,更多的cookie信息也被建立了

參考

Cross-Origin Resource Sharing Standard
Same-origin_policy

相關文章
相關標籤/搜索