1. 程式人生 > >HTTP訪問控制(CORS),解決跨域問題

HTTP訪問控制(CORS),解決跨域問題

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

正如大家所知,出於安全考慮,瀏覽器會限制指令碼中發起的跨域請求。比如,使用

 XMLHttpRequest 物件和Fetch發起 HTTP 請求就必須遵守同源策略。 具體而言,Web 應用程式通過 XMLHttpRequest 物件或Fetch能且只能向同域名的資源發起 HTTP 請求,而不能向任何其它域名發起請求。為了能開發出更強大、更豐富、更安全的Web應用程式,開發人員渴望著在不丟失安全的前提下,Web 應用技術能越來越強大、越來越豐富。比如,可以使用  發起跨站 HTTP 請求。(這段描述跨域不準確,跨域並非瀏覽器限制了發起跨站請求,而是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。最好的例子是CSRF跨站攻擊原理,請求是傳送到了後端伺服器無論是否跨域!注意:有些瀏覽器不允許從HTTPS的域跨域訪問HTTP,比如Chrome和Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例。

隸屬於 W3C 的 Web 應用工作組( Web Applications Working Group )推薦了一種新的機制,即跨源資源共享(Cross-Origin Resource Sharing (CORS))。這種機制讓Web應用伺服器能支援跨站訪問控制,從而使得安全地進行跨站資料傳輸成為可能。需要特別注意的是,這個規範是針對API容器的(比如說 或者 Fetch ),以減輕跨域HTTP請求的風險。

瀏覽器必須能支援跨源共享帶來的新的元件,包括請求頭和策略執行。同樣,伺服器端則需要解析這些新的請求頭,並按照策略返回相應的響應頭以及所請求的資源。這篇文章適用於網站管理員、伺服器端程式開發人員以及前端開發人員。對於伺服器端程式開發人員,還可以閱讀補充材料 

cross-origin sharing from a server perspective (with PHP code snippets) 。

  • 如上所述,使用 XMLHttpRequest 或 Fetch發起跨站 HTTP 請求。
  • WebGL 貼圖
  • 使用Images/video 畫面到canvas.
  • 樣式表(使用 CSSOM
  • Scripts (for unmuted exceptions).

接下來的文章,會對跨源資源共享做一個總覽,並介紹下在 Firefox 3.5 中已實現的跨源資源共享所使用的 HTTP 頭。

概述

跨源資源共享標準通過新增一系列 HTTP 頭,讓伺服器能宣告哪些來源可以通過瀏覽器訪問該伺服器上的資源。另外,對那些會對伺服器資料造成破壞性影響的 HTTP 請求方法(特別是 GET 以外的 HTTP 方法,或者搭配某些MIME型別的POST請求),標準強烈要求瀏覽器必須先以 OPTIONS 請求方式傳送一個預請求(preflight request),從而獲知伺服器端對跨源請求所支援 HTTP 方法。在確認伺服器允許該跨源請求的情況下,以實際的 HTTP 請求方法傳送那個真正的請求。伺服器端也可以通知客戶端,是不是需要隨同請求一起傳送信用資訊(包括 Cookies 和 HTTP 認證相關資料)。

隨後的章節,將對相關情景及用到的 HTTP 請求進行討論。

一些訪問控制場景

在此,我們會用三個場景來解釋跨源共享是怎麼執行的。其中,所有的跨站請求都是通過 物件發起。

如果對以下章節中的 JavaScript 程式碼片段感興趣,可以訪問這兒。在所有支援跨站 XMLHttpRequest 請求的瀏覽中,可以看到實際執行效果。而如果想繼續瞭解伺服器端對跨源請求的處理,則可以訪問這兒

簡單請求

所謂的簡單,是指:

  • 只使用 GET, HEAD 或者 POST 請求方法。如果使用 POST 向伺服器端傳送資料,則資料型別(Content-Type)只能是 application/x-www-form-urlencodedmultipart/form-data 或 text/plain中的一種。
  • 不會使用自定義請求頭(類似於 X-Modified 這種)。
Note: 這些跨站請求與以往瀏覽器發出的跨站請求並無異同。並且,如果伺服器不給出適當的響應頭,則不會有任何資料返回給請求方。因此,那些不允許跨站請求的網站無需為這一新的 HTTP 訪問控制特性擔心。

比如說,假如站點 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 行是 Firefox 3.5 發出的請求頭。注意看第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 指明的站點。

預請求

不同於上面討論的簡單請求,“預請求”要求必須先發送一個 OPTIONS 請求給目的站點,來查明這個跨站請求對於目的站點是不是安全可接受的。這樣做,是因為跨站請求可能會對目的站點的資料造成破壞。 當請求具備以下條件,就會被當成預請求處理:

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

Note: 從Gecko 2.0開始,text/plain, application/x-www-form-urlencoded 和 multipart/form-data型別的資料都可以直接用於跨站請求,而不需要先發起“預請求”了。之前,只有 text/plain 可以不用先發起“預請求”,進行跨站請求。

示例如下:

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

第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表明伺服器可以接受POSTGET和 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(); 
  }

如你所見,第七行程式碼將XMLHttpRequestwithCredentials標誌設定為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資訊也被建立了.

上面這些例子的執行可以檢視這裡.下一部分將討論實際的HTTP頭資訊.

HTTP響應頭

這部分裡列出了跨域資源共享(Cross-Origin Resource Sharing)時,伺服器端需要返回的響應頭資訊.上一部分內容是這部分內容在實際運用中的一個概述.

Access-Control-Allow-Origin

返回的資源需要有一個 Access-Control-Allow-Origin 頭資訊,語法如下:

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

origin引數指定一個允許向該伺服器提交請求的URI.對於一個不帶有credentials的請求,可以指定為'*',表示允許來自所有域的請求.

舉個例子,允許來自 http://mozilla.com 的請求,你可以這樣指定:

Access-Control-Allow-Origin: http://mozilla.com

如果伺服器端指定了域名,而不是'*',那麼響應頭的Vary值裡必須包含Origin.它告訴客戶端: 響應是根據請求頭裡的Origin的值來返回不同的內容的.

Access-Control-Expose-Headers

Requires Gecko 2.0(Firefox 4 / Thunderbird 3.3 / SeaMonkey 2.1)

設定瀏覽器允許訪問的伺服器的頭資訊的白名單:

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header

這樣, X-My-Custom-Header 和 X-Another-Custom-Header這兩個頭資訊,都可以被瀏覽器得到.

Access-Control-Max-Age

這個頭告訴我們這次預請求的結果的有效期是多久,如下:

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

delta-seconds 引數表示,允許這個預請求的引數快取的秒數,在此期間,不用發出另一條預檢請求. 

Access-Control-Allow-Credentials

告知客戶端,當請求的credientials屬性是true的時候,響應是否可以被得到.當它作為預請求的響應的一部分時,它用來告知實際的請求是否使用了credentials.注意,簡單的GET請求不會預檢,所以如果一個請求是為了得到一個帶有credentials的資源,而響應裡又沒有Access-Control-Allow-Credentials頭資訊,那麼說明這個響應被忽略了.

Access-Control-Allow-Credentials: true | false

帶有credential的請求在上面討論.

Access-Control-Allow-Methods

指明資源可以被請求的方式有哪些(一個或者多個). 這個響應頭資訊在客戶端發出預檢請求的時候會被返回. 上面有相關的例子.

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

發出預檢請求的例子見上,這個例子裡就有向客戶端傳送Access-Control-Allow-Methods響應頭資訊.

Access-Control-Allow-Headers

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

Access-Control-Allow-Headers: X-PINGOTHER

這樣在實際的請求裡,請求頭資訊裡就可以有這麼一條:

X-PINGOTHER: pingpong

可以有多個自定義HTTP請求頭,用逗號分隔.

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

HTTP 請求頭

這部分內容列出來當瀏覽器發出跨域請求時可能用到的HTTP請求頭.注意這些請求頭資訊都是在請求伺服器的時候已經為你設定好的,當開發者使用跨域的XMLHttpRequest的時候,不需要手動的設定這些頭資訊.

Origin

表明傳送請求或者預請求的域

Origin: <origin>

引數origin是一個URI,告訴伺服器端,請求來自哪裡.它不包含任何路徑資訊,只是伺服器名.

Note: Origin的值可以是一個空字串,這是很有用的.

注意,不僅僅是跨域請求,普通請求也會帶有ORIGIN頭資訊.

Access-Control-Request-Method

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

Access-Control-Request-Method: <method>

相關的例子可以在這裡找到

Access-Control-Request-Headers

在發出預檢請求時帶有這個頭資訊,告訴伺服器在實際請求時會攜帶的自定義頭資訊.如有多個,可以用逗號分開.

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

相關的例子可以在這裡找到

瀏覽器支援

Feature Chrome Firefox (Gecko) Internet Explorer Opera Safari
Basic support 4 3.5 8 (via XDomainRequest)
10
12 4

注意:

Internet Explorer 8 和 9 通過 XDomainRequest 物件來實現CORS ,但是在IE 10中有完整的實現。Firefox 3.5 就引入了對跨站 XMLHttpRequests 和 Web 字型的支援 ,儘管存在著一些直到後續版本才取消的限制。特別的, Firefox 7 引入了對跨站 WebGL 紋理的 HTTP 請求的支援,而且 Firefox 9 新增對通過 drawImage 在 canvas 上繪圖的支援。

相關連結

轉自:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS