由同源策略到前端跨域

同源策略 (Same-Origin Policy) 最先由 Netscape 公司提出, 所謂同源就是要求, 域名, 協議, 端口相同. 非同源的腳本不能訪問或者操做其餘域的頁面對象(如DOM等). 做爲著名的安全策略, 雖然它只是一個規範, 並不強制要求, 但如今全部支持 javaScript 的瀏覽器都會使用這個策略. 以致於該策略成爲瀏覽器最核心最基本的安全功能, 若是缺乏了同源策略, web的安全將無從談起.javascript

同源策略的限制

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

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

iframe限制

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

Ajax限制

Ajax 的限制比 iframe 限制更嚴.前端

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

Script限制

script並沒有跨域限制, 這是由於script標籤引入的文件不可以被客戶端的 js 獲取到, 不會影響到原頁面的安全, 所以script標籤引入的文件不必遵循瀏覽器的同源策略. 相反, ajax 加載的文件內容可被客戶端 js 獲取到, 引入的文件內容可能會泄漏或者影響原頁面安全, 故, ajax必須遵循同源策略.html5

注意

同源策略要求三同, 即: 同域, 同協議, 同端口.java

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

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

這裏須要澄清一個概念, 所謂的域, 跟 js 等資源的存放服務器沒有關係, 好比你到 baidu.com 使用 script 標籤請求了 google.com 下的js, 那麼該 js 所在域是 baidu.com, 而不是 google.com. 換言之, 它能操做baidu.com的頁面對象, 卻不能操做google.com的頁面對象.jquery

跨域訪問

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

使用代理

雖然ajax和iframe受同源策略限制, 但服務器端代碼請求, 卻不受此限制, 咱們能夠基於此去僞造一個同源請求, 實現跨域的訪問. 以下即是實現思路:github

  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/…

參考文章

相關文章
相關標籤/搜索