注意:以下代碼請在Firefox 3.5、Chrome 3.0、Safari 4之后的版本中進行測試。IE8的實現方法與其他瀏覽不同。
跨域請求,顧名思義,就是一個站點中的資源去訪問另外一個不同域名站點上的資源。這種情況很常見,比如說通過 style 標簽加載外部樣式表文件、通過 img 標簽加載外部圖片、通過 script 標簽加載外部腳本文件、通過 Webfont 加載字體文件等等。默認情況下,腳本訪問文檔屬性等數據采用的是同源策略(Same origin policy)。
那么,什么是同源策略呢?如果兩個頁面的協議、域名和端口是完全相同的,那么它們就是同源的。同源策略是為了防止從一個地址加載的文檔或腳本訪問或者設置從另外一個地址加載的文檔的屬性。如果兩個頁面的主域名相同,則還可以通過設置 document.domain 屬性將它們認為是同源的。
隨着 Web2.0 和 SNS 的興起,Web 應用對跨域訪問的需求也越來越多,但是,在腳本中進行跨域請求是受安全性限制的,Web 開發人員迫切需要提供一種更安全、方便的跨域請求方式來融合(Mashup)自己的 Web 應用。這樣做的一個好處就是可以將請求分攤到不同的服務器,減輕單個服務器壓力以提高響應速度;另外一個好處是可以將不同的業務邏輯分布到不同的服務器上以降低負載。
值得慶幸的是,跨域請求的標准已經出台,主流瀏覽器也已經實現了這一標准。W3C 工作組中的 Web Applications Working Group(Web 應用工作組)發布了一個 Cross-Origin Resource Sharing(跨域資源共享,該規范地址:http://www.w3.org/TR/access-control/和http://dev.w3.org/2006/waf/access-control/) 推薦規范來解決跨域請求的問題。該規范提供了一種更安全的跨域數據交換方法。具體規范的介紹可以訪問上面提供的網站地址。值得注意的是:該規范只能應用在類似 XMLHttprequest 這樣的 API 容器內。IE8、Firefox 3.5 及其以后的版本、Chrome瀏覽器、Safari 4 等已經實現了 Cross-Origin Resource Sharing 規范,已經可以進行跨域請求了。
Cross-Origin Resource Sharing 的工作方式是通過添加 HTTP 頭的方法來判斷哪些資源允許 Web 瀏覽器訪問該域名下的信息。然而,對於那些 HTTP 請求導致用戶數據產生副作用的請求方法(特別是對於除了GET、某些 MIME 類型的 POST 之外的 HTTP方法),該規范要求瀏覽器對請求進行“預先驗”,通過發送 HTTP 的 OPTIONS 請求頭詢問服務器有哪些支持的方法,在征得服務器的同意后,再使用實際的 HTTP 請求方法發送實際的請求。服務器也可以通知客戶端是否需要將驗證信息(如 Cookie 和 HTTP Authentication 數據)隨同請求一起發送。
下面我們就采用實際的例子說明 Cross-Origin Resource Sharing 是如何工作的。
1,簡單請求
什么樣的請求算是簡單請求呢?簡單請求必須滿足下面2點:
a,只使用 GET、POST 進行的請求,這里的POST只包括發送給服務器的數據類型(Content-Type)必須是 application/x-www-form-urlencoded、multipart/form-data 或者 text/plain中一個。
b,HTTP 請求沒有設置自定義的請求頭,如我們常用的 X-JSON。
先使用下面的代碼進行測試:
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
- <htmlxmlns="http://www.w3.org/1999/xhtml">
- <head>
- <title>孟憲會之AJAX跨域請求測試</title>
- </head>
- <body>
- <inputtype='button'value='開始測試'onclick='crossDomainRequest()'/>
- <divid="content"></div>
- <mce:scripttype="text/javascript"><!--
- var xhr =new XMLHttpRequest();
- var url ='http://dotnet.aspx.cc/SimpleCrossSiteRequests.aspx';
- function crossDomainRequest() {
- document.getElementById("content").innerHTML ="開始……";
- if (xhr) {
- xhr.open('GET', url, true);
- xhr.onreadystatechange =handler;
- xhr.send();
- } else {
- document.getElementById("content").innerHTML ="不能創建 XMLHttpRequest";
- }
- }
- function handler(evtXHR) {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
- var response =xhr.responseText;
- document.getElementById("content").innerHTML ="結果:" + response;
- } else {
- document.getElementById("content").innerHTML ="不允許跨域請求。";
- }
- }
- else {
- document.getElementById("content").innerHTML += "<br/>執行狀態 readyState:" + xhr.readyState;
- }
- }
- // --></mce:script>
- </body>
- </html>
- <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
- <html xmlns="http://www.w3.org/1999/xhtml">
- <head>
- <title>孟憲會之AJAX跨域請求測試</title>
- </head>
- <body>
- <input type='button' value='開始測試' onclick='crossDomainRequest()' />
- <div id="content"></div>
- <mce:script type="text/javascript"><!--
- var xhr = new XMLHttpRequest();
- var url = 'http://dotnet.aspx.cc/SimpleCrossSiteRequests.aspx';
- function crossDomainRequest() {
- document.getElementById("content").innerHTML = "開始……";
- if (xhr) {
- xhr.open('GET', url, true);
- xhr.onreadystatechange = handler;
- xhr.send();
- } else {
- document.getElementById("content").innerHTML = "不能創建 XMLHttpRequest";
- }
- }
- function handler(evtXHR) {
- if (xhr.readyState == 4) {
- if (xhr.status == 200) {
- var response = xhr.responseText;
- document.getElementById("content").innerHTML = "結果:" + response;
- } else {
- document.getElementById("content").innerHTML = "不允許跨域請求。";
- }
- }
- else {
- document.getElementById("content").innerHTML += "<br/>執行狀態 readyState:" + xhr.readyState;
- }
- }
- // --></mce:script>
- </body>
- </html>
然后,在服務器創建 CrossDomainRequest.aspx 的內容如下:
- <%@ PageLanguage="C#" %>
- <mce:scriptrunat="server"><!--
- protected void Page_Load(object sender, EventArgs e)
- {
- Response.AddHeader("Access-Control-Allow-Origin", "http://www.meng_xian_hui.com:801");
- Response.Write("孟憲會向各位朋友發來賀電:你的第一個跨域測試成功啦!!!");
- }
- // --></mce:script>
- <%@ Page Language="C#" %>
- <mce:script runat="server"><!--
- protected void Page_Load(object sender, EventArgs e)
- {
- Response.AddHeader("Access-Control-Allow-Origin", "http://www.meng_xian_hui.com:801");
- Response.Write("孟憲會向各位朋友發來賀電:你的第一個跨域測試成功啦!!!");
- }
- // --></mce:script>
點擊 “開始測試” 按鈕,發送的請求和返回的響應信息如下:
- GET /SimpleCrossSiteRequests.aspx HTTP/1.1
- Host: dotnet.aspx.cc
- User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.2; zh-CN; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
- Accept-Language: zh-cn,zh;q=0.5
- Accept-Encoding: gzip,deflate
- Accept-Charset: GB2312,utf-8;q=0.7,*;q=0.7
- Keep-Alive: 300
- Connection: keep-alive
- Referer: http://www.meng_xian_hui.com:801/CrossDomainAjax/SimpleCrossSiteRequests.html
- Origin: http://www.meng_xian_hui.com:801
- HTTP/1.x 200 OK
- Date: Sun, 10 Jan 2010 13:52:00 GMT
- Server: Microsoft-IIS/6.0
- X-Powered-By: ASP.NET
- X-AspNet-Version: 2.0.50727
- Access-Control-Allow-Origin: http://www.meng_xian_hui.com:801
- Set-Cookie: ASP.NET_SessionId=wk5v5nrs5wbfi4rmpjy2jujb;path=/; HttpOnly
- Cache-Control: private
- Content-Type: text/html; charset=utf-8
- Content-Length: 84
- GET /SimpleCrossSiteRequests.aspx HTTP/1.1
- Host: dotnet.aspx.cc
- User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.2; zh-CN; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
- Accept-Language: zh-cn,zh;q=0.5
- Accept-Encoding: gzip,deflate
- Accept-Charset: GB2312,utf-8;q=0.7,*;q=0.7
- Keep-Alive: 300
- Connection: keep-alive
- Referer: http://www.meng_xian_hui.com:801/CrossDomainAjax/SimpleCrossSiteRequests.html
- Origin: http://www.meng_xian_hui.com:801
- HTTP/1.x 200 OK
- Date: Sun, 10 Jan 2010 13:52:00 GMT
- Server: Microsoft-IIS/6.0
- X-Powered-By: ASP.NET
- X-AspNet-Version: 2.0.50727
- Access-Control-Allow-Origin: http://www.meng_xian_hui.com:801
- Set-Cookie: ASP.NET_SessionId=wk5v5nrs5wbfi4rmpjy2jujb; path=/; HttpOnly
- Cache-Control: private
- Content-Type: text/html; charset=utf-8
- Content-Length: 84
需要特別注意的是:在請求信息中,瀏覽器使用 Origin 這個 HTTP 頭來標識該請求來自於 http://www.meng_xian_hui.com:801;在返回的響應信息中,使用 Access-Control-Allow-Origin 頭來控制哪些域名的腳本可以訪問該資源。如果設置 Access-Control-Allow-Origin:*,則允許所有域名的腳本訪問該資源。如果有多個,則只需要使用逗號分隔開即可。
注意:在服務器端,Access-Control-Allow-Origin 響應頭 http://www.meng_xian_hui.com:801 中的端口信息不能省略。
有人可能會想:自己發送請求頭會如何呢?比如 xhr.setRequestHeader("Origin","http://www.meng_xian_hui.com:801"); 實踐證明,自己設置 Origin 頭是不行的。
是不是現在就可以采用 XMLHttpRequest 來請求任意一個網站的數據呢?還是不行的。允許哪些域名可以訪問,還需要服務器來設置 Access-Control-Allow-Origin 頭來進行授權,具體的代碼是:
Response.AddHeader("Access-Control-Allow-Origin", "http://www.meng_xian_hui.com:801");
這行代碼就告訴瀏覽器,只有來自 http://www.meng_xian_hui.com:801 源下的腳本才可以進行訪問。
好了,上面我們就完成了一個簡單的跨域請求,怎么樣?感覺還是不錯的吧。下面我們進行一個“預檢”請求。