由同源策略到前端跨域

路易斯發表於2017-04-20

同源策略 (Same-Origin Policy) 最早由 Netscape 公司提出, 所謂同源就是要求, 域名, 協議, 埠相同. 非同源的指令碼不能訪問或者操作其他域的頁面物件(如DOM等). 作為著名的安全策略, 雖然它只是一個規範, 並不強制要求, 但現在所有支援 javaScript 的瀏覽器都會使用這個策略. 以至於該策略成為瀏覽器最核心最基本的安全功能, 如果缺少了同源策略, web的安全將無從談起.

同源策略的限制

同源策略下的web世界, 域的壁壘高築, 從而保證各個網頁相互獨立, 互相之間不能直接訪問, iframe, ajax 均受其限制, 而script標籤不受此限制.

注: 以下如非特別說明, 均指非CORS的, 普通跨域請求.

iframe限制

  • 可以訪問同域資源, 可讀寫;
  • 訪問跨域頁面時, 只讀.

Ajax限制

Ajax 的限制比 iframe 限制更嚴.

  • 同域資源可讀寫;
  • 跨域請求會直接被瀏覽器攔截.(chrome下跨域請求不會發起, 其他瀏覽器一般是可傳送跨域請求, 但響應被瀏覽器攔截)

Script限制

script並無跨域限制, 這是因為script標籤引入的檔案不能夠被客戶端的 js 獲取到, 不會影響到原頁面的安全, 因此script標籤引入的檔案沒必要遵循瀏覽器的同源策略. 相反, ajax 載入的檔案內容可被客戶端 js 獲取到, 引入的檔案內容可能會洩漏或者影響原頁面安全, 故, ajax必須遵循同源策略.

注意

同源策略要求三同, 即: 同域, 同協議, 同埠.

  • 同域即host相同, 頂級域名, 一級域名, 二級域名, 三級域名等必須相同, 且域名不能與 ip 對應;
  • 同協議要求, http與https協議必須保持一致;
  • 同埠要求, 埠號必須相同.

IE有些例外, 它僅僅只是驗證主機名以及訪問協議,而忽略了埠號.

這裡需要澄清一個概念, 所謂的域, 跟 js 等資源的存放伺服器沒有關係, 比如你到 baidu.com 使用 script 標籤請求了 google.com 下的js, 那麼該 js 所在域是 baidu.com, 而不是 google.com. 換言之, 它能操作baidu.com的頁面物件, 卻不能操作google.com的頁面物件.

跨域訪問

實際上, 我們又不可避免地需要做一些跨域的請求, 下面提供幾種方案去繞過同源策略:

使用代理

雖然ajax和iframe受同源策略限制, 但伺服器端程式碼請求, 卻不受此限制, 我們可以基於此去偽造一個同源請求, 實現跨域的訪問. 如下便是實現思路:

  1. 請求同域下的web伺服器;
  2. web伺服器像代理一樣去請求真正的第三方伺服器;
  3. 代理拿到資料過後, 直接返回給客戶端ajax.

這樣, 我們便拿到了跨域資料.

JSONP

由上, script標籤並不受同源策略約束, 基於script 標籤可做 jsonp 形式的訪問, 可以通過第三方伺服器生成動態的js程式碼來回撥本地的js方法,而方法中的引數則由第三方伺服器在後臺獲取,並以JSON的形式填充到JS方法當中. 即 JSON with Padding. 具體如下:

1) 可用js生成以下html 程式碼, 去做jsonp的請求.

<script type="text/javascript" src="https://www.targetDomain.com/jsonp?callback=callbackName"></script>複製程式碼

使用 jquery, 即

jQuery.getJSON(
  "https://www.yourdomain.com/jsonp?callback=?",
  function(data) {
      console.log("name: " + data.name);
  }
);複製程式碼

其中回撥函式名 "callback" 為 "?", 即不需要使用者指定,而是由jquery生成.

2) 伺服器端,以 java 為例, 參考如下:

protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  //獲取JSON資料
  String jsonData = "{\"name\":\"jsonp\""}";
  //獲取回撥函式名
  String callback = req.getParameter("callback");  
  //拼接動態JS程式碼
  String output = callback + "(" + jsonData + ");
    resp.setContentType("text/javascript");
  PrintWriter out = resp.getWriter();
  out.println(output);
  // 響應為 callbackName({\"name\":\"jsonp\""});
}複製程式碼

postMessage

ES5新增的 postMessage() 方法允許來自不同源的指令碼採用非同步方式進行有限的通訊,可以實現跨文字檔、多視窗、跨域訊息傳遞.

語法: postMessage(data,origin)

data: 要傳遞的資料,html5規範中提到該引數可以是JavaScript的任意基本型別或可複製的物件,然而並不是所有瀏覽器都做到了這點兒,部分瀏覽器只能處理字串引數,所以我們在傳遞引數的時候建議使用JSON.stringify()方法對物件引數序列化,在低版本IE中引用json2.js可以實現類似效果.

origin:字串引數,指明目標視窗的源,協議+主機+埠號[+URL],URL會被忽略,所以可以不寫,這個引數是為了安全考慮,postMessage()方法只會將message傳遞給指定視窗,當然如果願意也可以建引數設定為"*",這樣可以傳遞給任意視窗,如果要指定和當前視窗同源的話設定為"/"。

父頁面傳送訊息:

window.frames[0].postMessage('message', origin)複製程式碼

iframe接受訊息:

window.addEventListener('message',function(e){
    if(e.source!=window.parent) return;//若訊息源不是父頁面則退出
      //TODO ...
});複製程式碼

其中 e 物件有三個重要的屬性

  • data, 表示父頁面傳遞過來的message
  • source, 表示傳送訊息的視窗物件
  • origin, 表示傳送訊息視窗的源(協議+主機+埠號)

CORS 跨域訪問

HTML5帶來了一種新的跨域請求的方式 — CORS, 即 Cross-origin resource sharing. 它更加安全, 上述的 JSONP, postMessage 等, 資源本身沒有能力保證自己不被濫用. CORS的目標是保護資源只被可信的訪問源以正確的方式訪問.

目前, 主流的瀏覽器都支援此協議, 可以在caniuse.com 中查到caniuse.com/#search=cor….

簡而言之, 瀏覽器不再一味禁止跨域訪問, 而是檢查目的站點的響應頭域, 進而判斷是否允許當前站點訪問. 通常, 伺服器使用以下的這些響應頭域用來通知瀏覽器:

Response headers[edit]
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
Access-Control-Allow-Headers
Access-Control-Expose-Headers
Access-Control-Max-Age複製程式碼

CORS的解決辦法是在服務端Response的HTTP頭域加入資源的訪問許可權資訊. 如: A站只需要在response頭中加一個欄位就能讓B站跨站訪問.

access-control-allow-origin:*複製程式碼

其中* 表示通配, 所有的域都能訪問此資源, 如果嚴謹一些只允許B站訪問:

access-control-allow-origin:<B-DOMAIN>複製程式碼

這樣B站就可以直接訪問此資源, 不需要JSONP 也不需要iframe了.

CORS需要指定METHOD訪問, 對於GET和POST請求, 至少要指定以下三種methods, 如下:

Access-Control-Allow-Methods: POST, GET, OPTIONS複製程式碼

如果是POST請求, 且提交的資料型別是json, 那麼, CORS需要指定headers.

Access-Control-Allow-Headers: Content-Type複製程式碼

CORS預設是不帶cookie的, 設定以下欄位將允許瀏覽器傳送cookie.

Access-Control-Allow-Credentials: true複製程式碼

除此之外, 為了跨站傳送cookie等驗證資訊, access-control-allow-origin 欄位將不允許設定為*, 它需要明確指定與請求網頁一致的域名.

同時, 請求網頁中需要做如下顯式設定才能真正傳送cookie.

xhr.withCredentials = true;複製程式碼

document.domain

通過修改document的domain屬性,我們可以在域和子域或者不同的子域之間通訊(即它們必須在同一個一級域名下). 同域策略認為域和子域隸屬於不同的域,比如a.com和 script.a.com是不同的域,這時,我們無法在a.com下的頁面中呼叫script.a.com中定義的JavaScript方法。但是當我們把它們document的domain屬性都修改為a.com,瀏覽器就會認為它們處於同一個域下,那麼我們就可以互相獲取對方資料或者操作對方DOM了。

比如, 我們在 www.a.com/a.html 下, 現在想獲取 www.script.a.com/b.html, 即主域名相同, 二級域名不同. 那麼可以這麼做:

document.domain = 'a.com';
var iframe = document.createElement('iframe');
iframe.src = 'http://www.script.a.com/b.html';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.addEventListener('load',function(){
    //TODO 載入完成時做的事情
    //var _document = iframe.contentWindow.document;
     //...
},false);複製程式碼

注意:

  • 2個頁面都要設定, 哪怕 a.html 頁已處於 a.com 域名下, 也必須顯式設定.
  • document.domain只能設定為一級域名,比如這裡a頁不能設定為www.a.com (二級域名).

利用domain屬性跨域具有以下侷限性:

  • 兩個頁面要在同一個一級域名下, 且必須同協議, 同埠, 即子域互跨;
  • 只適用於iframe.
Internet Explorer同源策略繞過

Internet Explorer8以及前面的版本很容易通過document.domain實現同源策略繞過, 通過重寫文件物件, 域屬性這個問題可以十分輕鬆的被利用.

var document;
document = {};
document.domain = 'http://www.a.com';
console.log(document.domain);複製程式碼

如果你在最新的瀏覽器中執行這段程式碼, 可能在JavaScript控制檯會顯示一個同源策略繞過錯誤.

window.name

window 物件的name屬性是一個很特別的屬性, 當該window的location變化, 然後重新載入, 它的name屬性可以依然保持不變. 那麼我們可以在頁面 A中用iframe載入其他域的頁面B, 而頁面B中用JavaScript把需要傳遞的資料賦值給window.name, iframe載入完成之後(iframe.onload), 頁面A修改iframe的地址, 將其變成同域的一個地址, 然後就可以讀出iframe的window.name的值了(因為A中的window.name和iframe中的window.name互相獨立的, 所以不能直接在A中獲取window.name, 而要通過iframe獲取其window.name). 這個方式非常適合單向的資料請求,而且協議簡單、安全. 不會像JSONP那樣不做限制地執行外部指令碼.

location.hash

location.hash(兩個iframe之間), 又稱FIM, Fragment Identitier Messaging的簡寫.

因為父視窗可以對iframe進行URL讀寫, iframe也可以讀寫父視窗的URL, URL有一部分被稱為hash, 就是#號及其後面的字元, 它一般用於瀏覽器錨點定位, Server端並不關心這部分, 所以這部分的修改不會產生HTTP請求, 但是會產生瀏覽器歷史記錄. 此方法的原理就是改變URL的hash部分來進行雙向通訊. 每個window通過改變其他 window的location來傳送訊息(由於兩個頁面不在同一個域下IE、Chrome不允許修改parent.location.hash的值,所以要藉助於父視窗域名下的一個代理iframe), 並通過監聽自己的URL的變化來接收訊息. 這個方式的通訊會造成一些不必要的瀏覽器歷史記錄, 而且有些瀏覽器不支援onhashchange事件, 需要輪詢來獲知URL的改變, 最後, 這樣做也存在缺點, 比如資料直接暴露在了url中, 資料容量和型別都有限等.

Access Control

此跨域方法目前只在很少的瀏覽器中得以支援, 這些瀏覽器可以傳送一個跨域的HTTP請求(Firefox, Google Chrome等通過XMLHTTPRequest實現, IE8下通過XDomainRequest實現), 請求的響應必須包含一個Access- Control-Allow-Origin的HTTP響應頭, 該響應頭宣告瞭請求域的可訪問許可權. 例如baidu.com對google.com下的getUsers.php傳送了一個跨域的HTTP請求(通過ajax), 那麼getUsers.php必須加入如下的響應頭:

header("Access-Control-Allow-Origin: http://www.baidu.com");//表示允許baidu.com跨域請求本檔案複製程式碼

flash URLLoder

flash有自己的一套安全策略, 伺服器可以通過crossdomain.xml檔案來宣告能被哪些域的SWF檔案訪問, SWF也可以通過API來確定自身能被哪些域的SWF載入. 當跨域訪問資源時, 例如從域 a.com 請求域 b.com上的資料, 我們可以藉助flash來傳送HTTP請求.

  • 首先, 修改域 b.com上的 crossdomain.xml(一般存放在根目錄, 如果沒有需要手動建立) , 把 a.com 加入到白名單;
<?xml version="1.0"?>
<cross-domain-policy>
<site-control permitted-cross-domain-policies="by-content-type"/>
<allow-access-from domain="a.com" />
</cross-domain-policy>複製程式碼
  • 其次, 通過Flash URLLoader傳送HTTP請求, 拿到請求後並返回;
  • 最後, 通過Flash API把響應結果傳遞給JavaScript.

Flash URLLoader是一種很普遍的跨域解決方案,不過需要支援iOS的話,這個方案就不可行了.

WebSocket

在WebSocket出現之前, 很多網站為了實現實時推送技術, 通常採用的方案是輪詢(Polling)和Comet技術, Comet又可細分為兩種實現方式, 一種是長輪詢機制, 一種稱為流技術, 這兩種方式實際上是對輪詢技術的改進, 這些方案帶來很明顯的缺點, 需要由瀏覽器對伺服器發出HTTP request, 大量消耗伺服器頻寬和資源. 面對這種狀況, HTML5定義了WebSocket協議, 能更好的節省伺服器資源和頻寬並實現真正意義上的實時推送.

WebSocket 本質上是一個基於TCP的協議, 它的目標是在一個單獨的持久連結上提供全雙工(full-duplex), 雙向通訊, 以基於事件的方式, 賦予瀏覽器實時通訊能力. 既然是雙向通訊, 就意味著伺服器端和客戶端可以同時傳送並響應請求, 而不再像HTTP的請求和響應. (同源策略對 web sockets 不適用)

原理: 為了建立一個WebSocket連線,客戶端瀏覽器首先要向伺服器發起一個HTTP請求, 這個請求和通常的HTTP請求不同, 包含了一些附加頭資訊, 其中附加頭資訊”Upgrade: WebSocket”表明這是一個申請協議升級的HTTP請求, 伺服器端解析這些附加的頭資訊然後產生應答資訊返回給客戶端, 客戶端和伺服器端的WebSocket連線就建立起來了, 雙方就可以通過這個連線通道自由的傳遞資訊, 並且這個連線會持續存在直到客戶端或者伺服器端的某一方主動的關閉連線.

一個典型WebSocket客戶端請求頭:

由同源策略到前端跨域
WebSocket客戶端請求頭


本問就討論這麼多內容,大家有什麼問題或好的想法歡迎在下方參與留言和評論.

本文作者: louis

本文連結: louiszhai.github.io/2016/03/02/…

參考文章

相關文章