專案中的積累——HTTP跨域問題方案CORS

在專案的開發中經常遇到所謂的跨域問題。最近也是遇到了這個問題,並且花了很多時間來解決這個問題。

前言

首先,什麼叫做跨域:
MDN上的解釋:

當一個資源請求一個其它域名的資源時會發起一個跨域HTTP請求(cross-origin HTTP
request)。比如說,域名A(http://domaina.example)的某 Web
應用通過標籤引入了域名B(http://domainb.foo)的某圖片資源(http://domainb.foo/image.jpg),域名A的 Web 應用就會導致瀏覽器發起一個跨域 HTTP 請求。在當今的 Web 開發中,使用跨域 HTTP
請求載入各類資源(包括CSS、圖片、JavaScript 指令碼以及其它類資源),已經成為了一種普遍且流行的方式。

我的理解是,其實跨域問題是瀏覽器的一個安全策略問題,因為瀏覽器認為跨域的訪問是不安全的,因此瀏覽器會限制指令碼中發起的跨域請求。
如果沒有做任何限制,在本地通過ajax訪問一個跨域源,檢視某些瀏覽器(chrome、firefox)的控制檯,就會發現報錯。
就會報這種錯誤
XMLHttpRequest cannot load http://192.168.0.25:8080/JsonAccept/accept. Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘null’ is therefore not allowed access

這裡寫圖片描述

什麼時候會出現跨域

那麼什麼時候會出現跨域問題呢?

URL(A)URL(B)說明是否允許跨域
http://www.a.com/a.jshttp://www.a.com/b.js同一域名下允許
http://www.a.com/lab/a.jshttp://www.a.com/script/b.js同一域名下不同資料夾允許
http://www.a.com:8000/a.jshttp://www.a.com/b.js同一域名,不同埠不允許
http://www.a.com/a.jshttps://www.a.com/b.js同一域名,不同協議不允許
http://www.a.com/a.jshttp://70.32.92.74/b.js域名和域名對應ip不允許
http://www.a.com/a.jshttp://script.a.com/b.js主域相同,子域不同不允許
http://www.a.com/a.jshttp://a.com/b.js同一域名,不同二級域名(同上)不允許(cookie這種情況下也不允許訪問)
http://www.cnblogs.com/a.jshttp://www.a.com/b.js不同域名不允許

一、CORS解決方案

CORS是一個W3C標準,全稱是”跨域資源共享”(Cross-origin resource sharing)。
它允許瀏覽器向跨源伺服器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

前提條件:CORS需要瀏覽器和伺服器同時支援。目前,所有瀏覽器都支援該功能,IE瀏覽器不能低於IE10。

整個CORS通訊過程,都是瀏覽器自動完成,不需要使用者參與。對於開發者來說,CORS通訊與同源的AJAX通訊沒有差別,程式碼完全一樣。

瀏覽器一旦發現AJAX請求跨源,就會自動新增一些附加的頭資訊,有時還會多出一次附加的請求,但使用者不會有感覺。

因此,實現CORS通訊的關鍵是伺服器。只要伺服器實現了CORS介面,就可以跨源通訊。

伺服器設定Access-Control-Allow-OriginHTTP響應頭之後,瀏覽器將會允許跨域請求

請求方式

  • 簡單請求
  • 複雜請求

簡單請求

所謂的簡單,是指:

只使用 GET, HEAD 或者 POST 請求方法。
如果使用 POST 向伺服器端傳送資料,則資料型別(Content-Type)只能是 application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一種。
不會使用自定義請求頭(類似於 X-Modified 這種)。
類似於下面這種情況
這裡寫圖片描述

網路請求資料

請求頭
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

可以看到響應頭,伺服器設定Access-Control-Allow-Origin: *,這表明伺服器接受來自任何站點的跨站請求。

如果伺服器端僅允許來自 http://foo.example 的跨站請求,它可以返回:Access-Control-Allow-Origin: http://foo.example

預請求

不同於上面討論的簡單請求,“預請求”要求必須先傳送一個 OPTIONS 請求給目的站點,來查明這個跨站請求對於目的站點是不是安全可接受的。
這樣做,是因為跨站請求可能會對目的站點的資料造成破壞。

當請求具備以下條件,就會被當成預請求處理:

1、請求以 GET, HEAD 或者 POST 以外的方法發起請求。或者,使用 POST,但請求資料為
application/x-www-form-urlencoded, multipart/form-data 或者 text/plain
以外的資料型別。比如說,用 POST 傳送資料型別為 application/xml 或者 text/xml 的 XML 資料的請求。
2、使用自定義請求頭(比如新增諸如 X-PINGOTHER)

*第一次請求*
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
*第二次返回*
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]

這種情況使用一個 OPTIONS 傳送了一個“預請求”。
Firefox 3.1 根據請求引數,決定需要傳送一個“預請求”,來探明伺服器端是否接受後續真正的請求。

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。這樣,伺服器就可以決定,在當前情況下,是否接受該跨站請求訪問。

該響應表明,伺服器接受了客服端的跨站請求。返回的程式碼:

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天內,瀏覽器在處理針對該伺服器的跨站請求,都可以無需再傳送“預請求”,只需根據本次結果進行判斷處理。

附帶憑證資訊的請求

前面的跨域問題得到解決了,但是又發現一個問題,就是cookie無法攜帶問題。
原因在此:
XMLHttpRequest和訪問控制功能,最有趣的特性就是,傳送憑證請求(HTTP Cookies和驗證資訊)的功能。一般而言,對於跨站請求,瀏覽器是不會傳送憑證資訊的。但如果將XMLHttpRequest的一個特殊標誌位設定為true,瀏覽器就將允許該請求的傳送。
方法:
伺服器設定

Access-Control-Allow-Credentials: true

另一方面,開發者必須在AJAX請求中開啟withCredentials屬性。

如果是用的jq的ajax請求,就在ajax請求中加上這樣的語句:

  xhrFields: {
withCredentials: true
},
crossDomain: true,
 $.ajax({
type: "POST",
url: url,
contentType: "application/json; charset=utf-8",
dataType: 'json',
xhrFields: {
withCredentials: true
},
crossDomain: true,
success: function(result) {
}
});

如果是XMLHTTP請求方式

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

值得注意的是:
如果要傳送Cookie,Access-Control-Allow-Origin就不能設為星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用伺服器域名設定的Cookie才會上傳,其他域名的Cookie並不會上傳,且(跨源)原網頁程式碼中的document.cookie也無法讀取伺服器域名下的Cookie。

看一下指令碼互動

請求頭
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資訊也被建立了.