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請求的風險。
瀏覽器必須能支援跨源共享帶來的新的元件,包括請求頭和策略執行。同樣,伺服器端則需要解析這些新的請求頭,並按照策略返回相應的響應頭以及所請求的資源。這篇文章適用於網站管理員、伺服器端程式開發人員以及前端開發人員。對於伺服器端程式開發人員,還可以閱讀補充材料
- 如上所述,使用
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-urlencoded
,multipart/form-data 或 text/plain
中的一種。 - 不會使用自定義請求頭(類似於 X-Modified 這種)。
比如說,假如站點 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
以外的資料型別。比如說,用 POST 傳送資料型別為, multipart/form-data 或者
text/plain
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表明伺服器可以接受
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資訊也被建立了.
上面這些例子的執行可以檢視這裡.下一部分將討論實際的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 上繪圖的支援。