Cross-Origin-Resource-Sharing-Solutions
from:https://github.com/hijiangtao/hijiangtao.github.io/blob/master/_posts/2017-06-13-Cross-Origin-Resource-Sharing-Solutions.md
title | layout | thread | date | author | categories | tags | excerpt | |||
---|---|---|---|---|---|---|---|---|---|---|
前端跨域請求解決方案匯總 | post | 175 | 2017-06-13 | Joe Jiang | documents |
|
同源策略限制從一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的關鍵的安全機制。但是有時候跨域請求資源是合理的需求,本文嘗試從多篇文章中匯總至今存在的所有跨域請求解決方案。 |
同源策略限制從一個源加載的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的關鍵的安全機制。但是有時候跨域請求資源是合理的需求,本文嘗試從多篇文章中匯總至今存在的所有跨域請求解決方案。
跨域請求
首先需要了解的是同源和跨源的概念。對於相同源,其定義為:如果協議、端口(如果指定了一個)和主機對於兩個頁面是相同的,則兩個頁面具有相同的源。只要三者之一任意一點有不同,那麽就為不同源。當一個資源從與該資源本身所在的服務器的域或端口不同的域或不同的端口請求一個資源時,資源會發起一個跨域 HTTP 請求。而有關跨域請求受到限制的原因可以參考如下 MDN 文檔片段:
跨域不一定是瀏覽器限制了發起跨站請求,而也可能是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。最好的例子是 CSRF 跨站攻擊原理,請求是發送到了後端服務器無論是否跨域!註意:有些瀏覽器不允許從 HTTPS 的域跨域訪問 HTTP,比如 Chrome 和 Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例。
解決方法匯總
以下我們由簡及深介紹各種存在的跨域請求解決方案,包括 document.domain, location.hash, window.name, window.postMessage, JSONP, WebSocket, CORS
。
document.domain
document.domain
的作用是用來獲取/設置當前文檔的原始域部分,例如:
// 對於文檔 www.example.xxx/good.html
document.domain="www.example.xxx"
// 對於URI http://developer.mozilla.org/en/docs/DOM
document.domain="developer.mozilla.org"
如果當前文檔的域無法識別,那麽 domain 屬性會返回 null。
在根域範圍內,Mozilla允許你把domain屬性的值設置為它的上一級域。例如,在 developer.mozilla.org 域內,可以把domain設置為 "mozilla.org" 但不能設置為 "mozilla.com" 或者"org"。
因此,若兩個源所用協議、端口一致,主域相同而二級域名不同的話,可以借鑒該方法解決跨域請求。
比如若我們在 http://a.github.io 頁面執行以下語句:
document.domain = "github.io"
那麽之後頁面對 github.io
發起請求時頁面則會成功通過對 github.io
的同源檢測。比較直接的一個操作是,當我們在a.github.io
頁面中利用 iframe 去加載 github.io
時,通過如上的賦值後,我們可以在 a.github.io
頁面中去操作 iframe 裏的內容。
我們同時考慮另一種情況:存在兩個子域名 a.github.io
以及 b.github.io
, 其中前者域名下網頁 a.html 通過 iframe 引入了後者域名下的 b.html,此時在 a.html 中是無法直接操作 b.html 的內容的。
同樣利用 document.domain
,我們在兩個頁面中均加入
document.domain=‘github.io‘
這樣在以上的 a.html 中就可以操作通過 iframe 引入的 b.html 了。
document.domain 的優點在於解決了主語相同的跨域請求,但是其缺點也是很明顯的:比如一個站點受到攻擊後,另一個站點會因此引起安全漏洞;若一個頁面中引入多個 iframe,想要操作所有的 iframe 則需要設置相同的 domain。
location.hash
location.hash
是一個可讀可寫的字符串,該字符串是 URL 的錨部分(從 # 號開始的部分)。例如:
// 對於頁面 http://example.com:1234/test.htm#part2
location.hash = "#part2"
同時,由於我們知道改變 hash 並不會導致頁面刷新,所以可以利用 hash 在不同源間傳遞數據。
假設 github.io
域名下 a.html 和 shaonian.eu
域名下 b.html 存在跨域請求,那麽利用 location.hash 的一個解決方案如下:
- a.html 頁面中創建一個隱藏的 iframe, src 指向 b.html,其中 src 中可以通過 hash 傳入參數給 b.html
- b.html 頁面在處理完傳入的 hash 後通過修改 a.html 的 hash 值達到將數據傳送給 a.html 的目的
- a.html 頁面添加一個定時器,每隔一定時間判斷自身的 location.hash 是否變化,以此響應處理
以上步驟中需要註意第二點:如何在 iframe 頁面中修改 父親頁面的 hash 值。由於在 IE 和 Chrome 下,兩個不同域的頁面是不允許 parent.location.hash
這樣賦值的,所以對於這種情況,我們需要在父親頁面域名下添加另一個頁面來實現跨域請求,具體如下:
- 假設 a.html 中 iframe 引入了 b.html, 數據需要在這兩個頁面之間傳遞,且 c.html 是一個與 a.html 同源的頁面
- a.html 通過 iframe 將數據通過 hash 傳給 b.html
- b.html 通過 iframe 將數據通過 hash 傳給 c.html
- c.html 通過
parent.parent.location.hash
設置 a.html 的 hash 達到傳遞數據的目的
location.bash 方法的優點在於可以解決域名完全不同的跨域請求,並且可以實現雙向通訊;而缺點則包括以下幾點:
- 利用這種方法傳遞的數據量受到 url 大小的限制,傳遞數據類型有限
- 由於數據直接暴露在 url 中則存在安全問題
- 若瀏覽器不支持
onhashchange
事件,則需要通過輪訓來獲知 url 的變化 - 有些瀏覽器會在 hash 變化時產生歷史記錄,因此可能影響用戶體驗
window.name
該屬性用於獲取/設置窗口的名稱。其特征在於:一個窗口的生命周期內,窗口載入的所有頁面共享該值,且都具有對該屬性的讀寫權限。這意味著如果不修改該值,那麽在不同頁面加載之後該值也不會變,且其支持長達 2MB 的存儲量。
利用該特性我們可以將跨域請求用如下步驟解決:
- 在 a.github.io/a.html 中創建 iframe 指向 b.github.io/b.html (頁面會將自身的 window.name 附在 iframe 上)
- 給 a.github.io/a.html 添加監聽 iframe 的 onload 事件,在該事件中將 iframe 的 src 設置為本地域的代理文件(代理文件和a.html處於同一域下,可以相互通信),同時可以傳出 iframe 的 name 值
- 獲取數據後銷毀 iframe,釋放內存,同時也保證了安全
window.name 的優勢在於巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。
window.postMessage
HTML5 為了解決這個問題,引入了一個全新的 API:跨文檔通信 API(Cross-document messaging)。這個 API 為 window 對象新增了一個 window.postMessage 方法,允許跨窗口通信,不論這兩個窗口是否同源。
API 的詳細使用方法請見 MDN。
JSONP
JSONP, 全稱 JSON with Padding,是使用 AJAX 實現的請求不同源的跨域。其基本原理:網頁通過添加一個 <script>
元素,向服務器請求 JSON 數據,這種做法不受同源政策限制;服務器收到請求後,將數據放在一個指定名字的回調函數裏傳回來。
以下為一個例子,由於 test.js 返回的內容直接作為代碼運行,所以只要 a.html 中定義了 callback
函數, 它就會立即被調用。
// 當前頁面 a.com/a.html
<script type="text/javascript">
//回調函數
function callback(data) {
alert(data.message);
}
</script>
<script type="text/javascript" src="http://b.com/test.js"></script>
// test.js
// 調用callback函數,並以json數據形式作為闡述傳遞,完成回調
callback({message:"success"});
為了保證 script 的靈活,我們可以通過 JavaScript 動態創建 script 標簽,並通過 HTTP 參數向服務器傳入回調函數名,案例如下所示:
<script type="text/javascript">
// 添加<script>標簽的方法
function addScriptTag(src){
var script = document.createElement(‘script‘);
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function(){
// 搜索apple,將自定義的回調函數名result傳入callback參數中
addScriptTag("http://ajax.googleapis.com/ajax/services/search/web?v=1.0&q=apple&callback=result");
}
// 自定義的回調函數result
function result(data) {
// 我們就簡單的獲取apple搜索結果的第一條記錄中url數據
alert(data.responseData.results[0].unescapedUrl);
}
</script>
jQuery 有相應的 JSONP 的實現方法,見 API。
JSONP的優點在於簡單適用,老式瀏覽器全部支持,服務器改造小。不需要XMLHttpRequest或ActiveX的支持;但缺點是只支持 GET 請求。
WebSocket
WebSocket 協議不實行同源政策,只要服務器支持,就可以通過它進行跨源通信。
CORS
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
跨域資源共享( CORS )機制允許 Web 應用服務器進行跨域訪問控制,從而使跨域數據傳輸得以安全進行。其需要服務端和客戶端同時支持。
跨域資源共享標準( cross-origin sharing standard )允許在下列場景中使用跨域 HTTP 請求:
由 XMLHttpRequest 或 Fetch 發起的跨域 HTTP 請求
Web 字體 (CSS 中通過 @font-face 使用跨域字體資源), 因此,網站就可以發布 TrueType 字體資源,並只允許已授權網站進行跨站調用
WebGL 貼圖
使用 drawImage 將 Images/video 畫面繪制到 canvas
樣式表(使用 CSSOM)
Scripts (未處理的異常)
CORS 存在以下三種主要場景,分別是簡單請求,預檢請求和附帶身份憑證的請求。
- 簡單請求:若只使用 GET, HEAD 或者 POST 請求,且除 CORS 安全的首部字段集合外,無人為設置該集合之外的其他首部字段,同時 Content-Type 值屬於下列之一,那麽該請求則可以被視為簡單請求:
application/x-www-form-urlencoded
multipart/form-data
text/plain
此情況下,若服務端返回的 Access-Control-Allow-Origin: *
,則表明該資源可以被任意外域訪問。若要指定僅允許來自某些域的訪問,需要將 *
設定為該域,例如:
Access-Control-Allow-Origin: http://foo.example
- 預檢請求:與前述簡單請求不同,該要求必須首先使用 OPTIONS 方法發起一個預檢請求到服務器,以獲知服務器是否允許該實際請求。當請求滿足以下三個條件任意之一時, 即應首先發送預檢請求:
- 使用了 PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 中任一的 HTTP 方法
- 人為設置了對 CORS 安全的首部字段集合之外的其他首部字段
- Content-Type 的值不屬於下列之一
application/x-www-form-urlencoded
multipart/form-data
text/plain
預檢請求完成之後(通過 OPTIONS 方法實現),才發送實際請求。一個示範 HTTP 請求如下所示:
var invocation = new XMLHttpRequest();
var url = ‘http://bar.other/resources/post-here/‘;
var body = ‘<?xml version="1.0"?><person><name>Arun</name></person>‘;
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);
}
}
- 附帶身份憑證的請求:這種方式的特點在於能夠在跨域請求時向服務器發送憑證請求,例如 Cookies (withCredentials 標誌設置為 true)。
一般而言,對於跨域 XMLHttpRequest 或 Fetch 請求,瀏覽器不會發送身份憑證信息。如果要發送憑證信息,需要設置 XMLHttpRequest 的某個特殊標誌位。但是需要註意的是,如果服務器端的響應中未攜帶 Access-Control-Allow-Credentials: true
,瀏覽器將不會把響應內容返回給請求的發送者。
附帶身份憑證的請求與通配符
對於附帶身份憑證的請求,服務器不得設置 Access-Control-Allow-Origin 的值為“*”。
這是因為請求的首部中攜帶了 Cookie 信息,如果 Access-Control-Allow-Origin 的值為“*”,請求將會失敗。而將 Access-Control-Allow-Origin 的值設置為 http://foo.example,則請求將成功執行。
另外,響應首部中也攜帶了 Set-Cookie 字段,嘗試對 Cookie 進行修改。如果操作失敗,將會拋出異常。
MDN 引例如下:
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();
}
}
其實由上我們知道,CORS 的優點也非常明顯:CORS支持所有類型的HTTP請求,是跨域HTTP請求的根本解決方案。
以上就是所有的跨域請求解決方案,根據實際生產環境,總有一款適合你。
參考
- https://github.com/wengjq/Blog/issues/2
- https://developer.mozilla.org/zh-CN/docs/Web/Security/Same-origin_policy
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
- http://www.cnblogs.com/zichi/p/4620656.html
Cross-Origin-Resource-Sharing-Solutions