本文主要涉及三種跨域方法:JSONP、CORS、postMessage。
Q:為什麼會出現跨域問題?
A:出於瀏覽器的同源策略限制,瀏覽器會拒絕跨域請求。
*注:嚴格的說,瀏覽器並不是拒絕所有的跨域請求,實際上拒絕的是跨域的讀操作。瀏覽器的同源限制策略是這樣執行的:
- 通常瀏覽器允許進行跨域寫操作(Cross-origin writes),如連結,重定向;
- 通常瀏覽器允許跨域資源嵌入(Cross-origin embedding),如 img、script 標籤;
- 通常瀏覽器不允許跨域讀操作(Cross-origin reads)。*
Q:什麼情況才算作跨域?
A:非同源請求,均為跨域。名詞解釋:同源 —— 如果兩個頁面擁有相同的協議(protocol),埠(port)和主機(host),那麼這兩個頁面就屬於同一個源(origin)。Q:為什麼有跨域需求?
A:場景 —— 工程服務化後,不同職責的服務分散在不同的工程中,往往這些工程的域名是不同的,但一個需求可能需要對應到多個服務,這時便需要呼叫不同服務的介面,因此會出現跨域。
如何實現跨域
通常,最常用的跨域方式有以下三種:JSONP、CORS、postMessage。
JSONP
單純地為了實現跨域請求而創造的一個 trick。
【實現原理】
雖然因為同源策略的影響,不能通過XMLHttpRequest請求不同域上的資料(Cross-origin reads)。但是,在頁面上引入不同域上的js指令碼檔案卻是可以的(Cross-origin embedding)。因此在js檔案載入完畢之後,觸發回撥,可以將需要的data作為引數傳入。
【實現方式(需前後端配合)】
<script type="text/javascript">
function dosomething(data){
//處理獲得的資料
}
</script>
<script src="http://example.com/data.php?callback=dosomething"></script>複製程式碼
<?php
$callback = $_GET['callback'];//得到回撥函式名
$data = array('a','b','c');//要返回的資料
echo $callback.'('.json_encode($data).')';//輸出
?>複製程式碼
【JSONP的優缺點】
優點:相容性好(相容低版本IE)
缺點:1.JSONP只支援GET請求; 2.XMLHttpRequest相對於JSONP有著更好的錯誤處理機制
CORS
CORS 是W3C 推薦的一種新的官方方案,能使伺服器支援 XMLHttpRequest 的跨域請求。CORS 實現起來非常方便,只需要增加一些 HTTP 頭,讓伺服器能宣告允許的訪問來源。
值得注意的是,通常使用CORS時,非同步請求會被分為簡單請求和非簡單請求,非簡單請求的區別是會先發一次預檢請求。
【簡單請求】
使用下列方法之一且沒有人為設定對 CORS 安全的首部欄位集合之外的其他首部欄位:
- GET
- HEAD
- POST
- 僅當POST方法的Content-Type值等於下列之一才算作簡單請求 - text/plain - multipart/form-data - application/x-www-form-urlencoded複製程式碼
請求報文:
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複製程式碼
請求報文的第10行:Origin: foo.example 表明該請求來源於 foo.exmaple。
響應報文:
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]複製程式碼
響應報文的第4行:Access-Control-Allow-Origin: * 表明該資源可以被任意外域訪問。
【非簡單請求】
- 使用了下面任一 HTTP 方法:
- PUT
- DELETE
- CONNECT
- OPTIONS
- TRACE
- PATCH
- 人為設定了對 CORS 安全的首部欄位集合之外的其他首部欄位。該集合為:
- Accept
- Accept-Language
- Content-Language
- Content-Type (but note the additional requirements below)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- Content-Type 的值不屬於下列之一:
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
傳送真正請求前會先傳送預檢請求,如圖所示:
1.第一條OPTIONS為預檢請求,中同時攜帶了下面兩個首部欄位:
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER複製程式碼
預檢請求的Request中的Access-Control-Request-Method: POST,是告訴伺服器,之後的實際請求將使用POST方式。
Access-Control-Request-Headers 是告訴伺服器,實際請求將攜帶兩個自定義請求首部欄位:X-PINGOTHER 與 Content-Type。伺服器據此決定,該實際請求是否被允許
預檢請求的Response中的
Access-Control-Allow-Origin: foo.example // 標識可接受的跨域請求源;
Access-Control-Allow-Methods: POST, GET, OPTIONS //標識可接受的跨域請求方法,如GET、POST、OPTIONS;
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type //標識可接受的跨域請求自定義頭;
Access-Control-Max-Age: 86400。 //標識本次預請求的有效時間(秒),期間內無需再傳送預請求;
XMLHttpRequest 請求可以傳送憑證請求(HTTP Cookies 和驗證資訊),通常不會跨域傳送憑證資訊,但也有一些情況需要打通不同的登入態,因此如果要傳送憑證資訊,需要設定 XMLHttpRequest 的某個特殊標誌位。比如下面程式碼,可以把 XMLHttpRequest 的 withCredentials 設定為 true,這樣瀏覽器就能跨域傳送憑證資訊。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;複製程式碼
服務端返回的響應頭中的 Access-Control-Allow-Credentials 欄位存在且為 true 時,瀏覽器才會將響應結果傳遞給客戶端程式。另外,Access-Control-Allow-Origin 必須指定請求源的域名,否則響應失敗。
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://foo.com
Access-Control-Allow-Credentials: true
Set-Cookie: pageAccess=3; expires=Wed, 31-Dec-2008 01:34:53 GMT
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain複製程式碼
如下圖所示為附帶身份憑證的請求流程圖:
postMessage
window.postMessage(message,targetOrigin) 方法是html5新引進的特性,可以使用它來向其它的window物件傳送訊息,無論這個window物件是屬於同源或不同源,目前IE8+、FireFox、Chrome、Opera等瀏覽器都已經支援window.postMessage方法。
otherWindow.postMessage(message, targetOrigin, [transfer]);複製程式碼