同源策略及其解決方案

「同源政策」是瀏覽器安全的基石,其設計目的是爲了保證信息安全,防止惡意的網站竊取數據。所謂「同源」必須知足如下三個方面:javascript

  1. 協議相同
  2. 域名相同
  3. 端口相同(默認端口是80,能夠省略)

若是是非同源的,如下行爲會受到限制:php

  • Cookie、LocalStorageIndexDB沒法讀取
  • DOM沒法獲取
  • AJAX請求不能發送

接下來咱們主要講解如何解決以上三個方面的問題。html

1、Cookie

Cookie只有同源的網站才能獲取,可是若是兩個網頁的一級域名相同,只是二級域名不一樣,能夠設置相同的document.domain,兩個網頁就能夠共享cookie了。java

不少人都誤把帶www當成一級域名,把其餘前綴的當成二級域名,是錯誤的。正確的域名劃分爲:json

  1. 頂級域名:.com
  2. 一級域名:baidu.com
  3. 二級域名:tieba.baidu.com

舉例來講,A網頁是http://w1.sillywa.com/a.html,B網頁是http://w2.sillywa.com/b.html,咱們能夠設置跨域

document.domain = 'sillywa.com'
複製代碼

這樣兩個網頁就能夠共享Cookie了。瀏覽器

注意,這種方法只是用於CookieiframeLocalStorageIndexDB沒法經過這種方法規避同源政策,而是要是用PostMessage API,下面咱們會介紹。安全

2、iframe

若是兩個網頁不一樣源,就無法拿到對方的DOM。典型的例子是iframe窗口和用window.open方法打開的窗口,它們與父窗口沒法通訊。bash

因此對於徹底不一樣源的網站,目前可使用一下三種辦法規避同源問題:服務器

  • 片斷標識符(fragment identifier)
  • window.name
  • 跨文檔通訊API(window.postMessage)

1.片斷標識符

片斷標識符指的是URL#後面的內容,好比http://sillywa.com/a.html#fragment中的#fragment,若是隻是改變片斷標識符,頁面不會從新刷新。

父窗口能夠把信息寫入子窗口的片斷標識符:

var src = originURL + '#' + data
document.getElementById('myIframe').src = src
複製代碼

子窗口經過監聽hashchange事件獲得通知:

window.onhashchange = function() {
    console.log(window.location.hash)
}
複製代碼

2.window.name

瀏覽器窗口有window.name屬性。這個屬性的最大特色是,不管是否同源,只要在同一個窗口裏,前一個網頁設置了這個屬性,後一個網頁能夠讀取它。

3. window.postMessage

HTML5爲了解決跨窗口通信問題引入了一個新的API:跨文檔通訊API。這個APIwindow新增了一個window.postMessage()方法,容許跨窗口通信,不論這兩個窗口是否同源。舉例來講:假設父窗口爲:http://aaa.com,子窗口爲:http://bbb.com

// 父窗口向子窗口發送消息
var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');
複製代碼

postMessage()方法的第一個參數是具體的信息內容,第二個參數是接收消息的窗口的源(origin),即"協議 + 域名 + 端口"。也能夠設爲*,表示不限制域名,向全部窗口發送。

一樣,子窗口向父窗口發送消息能夠這樣寫:

window.opener.postMessage('Nice to see you', 'http://aaa.com');
複製代碼

父窗口和子窗口均可以經過message事件,監聽對方的消息:

window.addEventListener('message', function(e) {
    console.log(e.data)
},false)
複製代碼

message事件的event對象有如下三個屬性:

  1. event.source:發送消息的窗口
  2. event.origin:消息發送的網址
  3. event.data:消息內容

下面的例子是,子窗口經過event.source屬性引用父窗口,而後發送消息。

window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
  event.source.postMessage('Nice to see you!', '*');
}
複製代碼

若是咱們將發送的消息改成LocalStorage,則能夠互相讀取LocalStorage

3、AJAX

一樣AJAX請求也會受到同源策略的影響,除了使用代理服務器外,還有一下方法能夠實現跨域:

  • jsonp
  • WebScoket
  • CORS

1.jsonp

jsonp想必你們都很瞭解,其由兩部分組成:回調函數和數據。其基本思路是:動態插入script標籤,向服務器請求json數據,返回的數據將在回調函數裏得到。

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}
// 定義回調函數
function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}
複製代碼

上面代碼經過動態添加<script>元素,向服務器example.com發出請求。注意,該請求的查詢字符串有一個callback參數,用來指定回調函數的名字,這對於JSONP是必需的。

2.WebScoket

WebScoket不一樣於http,它提供一種雙向通信的功能,即客戶端能夠向服務器請求數據,同時服務器也能夠向客戶端發送數據。而http只能是單向的。

同時WebScoket使用ws:\//(非加密)和wss:\//(加密)做爲協議前綴。該協議不實行同源政策,只要服務器支持,就能夠經過它進行跨源通訊。

要建立WebScoket,先實例化一個WebScoket對象並傳入要鏈接的URL

var scoket = new WebScoket("ws://www.example.com/server.php")
複製代碼

實例化WebScoket對象以後,瀏覽器會立刻嘗試創建鏈接。與XHR相似,WebScoket也有一系列表示當前狀態的readyState屬性,以下:

  • WebScoket.OPENING (0):正在創建鏈接
  • WebScoket.OPEN (1):已經創建鏈接
  • WebScoket.CLOSING (2):正在關閉鏈接
  • WebScoket.ClOSE (3):已經關閉鏈接

WebScoket沒有readyStatechange事件;不過它有其餘的事件,咱們待會介紹。

要關閉WebScoket鏈接,能夠調用close()方法:

scoket.close()
複製代碼

WebScoket鏈接以後,就能夠發送和就收數據。要發送數據能夠調用send()方法,並傳入字符串,例如:

var scoket = new WebScoket("ws://www.example.com/server.php")
scoket.send('hello word')
複製代碼

由於WebScoket只能發送純文本數據,因此對於複雜的數據類型咱們應先將其序列化轉化爲json字符串

var message = {
    name: 'sillywa'
}
scoket.send(JSON.stringify(message))
複製代碼

一樣服務器必須先解析再讀取數據。

當服務器向客戶端發來消息時,WebScoket對象就會觸發message事件。這個message事件與其它傳遞消息的協議相似,也就是把返回的數據保存在event.data的屬性中。

scoket.onmessage = function(event) {
    console.log(event.data)
}
複製代碼

與經過send()發送到服務器的數據同樣,event.data中返回的數據也是字符串。

WebScoket對象還有其餘三個事件,在鏈接生命週期的不一樣階段觸發。

  • open:在成功創建鏈接時觸發
  • error:在發生錯誤時觸發,鏈接不能持續
  • close:在鏈接關閉時觸發 WebScoket對象不支持DOM2級事件偵聽器,所以必須使用DOM0級語法分別定義每一個事件處理程序。
var scoket = new WebScoket("ws://www.example.com/server.php")
scoket.onopen = function() {
    console.log('connection start')
}
scoket.onerror = function() {
    console.log('connection error')
}
scoket.onclose = function(event) {
    console.log(event)
}
複製代碼

在這三個事件中只有closeevent對象有額外的信息。這個事件的對象有三個額外的屬性:wasClean、code、reason。其中wasClean是一個布爾值,表示鏈接是否已經明確地關閉;code是服務器返回的數值狀態碼;reason是一個字符串,包含服務器發回的信息。

3.CORS

CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。

它容許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

相比jsonp只能發送get請求,CORS容許發送任何類型的請求。但CORS要求瀏覽器和服務器同時支持。目前全部瀏覽器都支持,IE須要IE10以上。

整個CORS通信過程當中都是瀏覽器自動完成,不須要用戶的參與。CORS通信和同源的AJAX請求沒有區別。瀏覽器一旦發現AJAX請求跨域,就會自動添加一些頭部信息,有時候還會多出一次附加請求。

瀏覽器將CORS請求分爲兩類:簡單請求和非簡單請求。

只要同時知足一下兩個條件就是簡單請求,不然就是非簡單請求:

(1)請求方法是下列方法之一:

  • HEAD
  • GET
  • POST

(2)http的頭信息不超出如下幾個字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain

對於簡單請求,瀏覽器會自動在頭部信息裏增長一個Origin字段,用來表示請求來自與哪一個源,服務器根據這個值決定是否贊成這次請求。若是Origin不在請求範圍內,服務器返回一個正常的http迴應。這個迴應的頭信息中沒有Access-Control-Allow-Origin字段,瀏覽器發現沒有這個字段以後就會拋出一個錯誤。若是Origin在請求範圍內,服務器返回的響應會多出幾個頭信息字段,其中一個是Access-Control-Allow-Origin,它的值要麼是Origin的值,要麼是*,表示容許任何域名的請求。

對於非簡單請求,它會在正式通訊以前,增長一次http查詢請求,稱爲"預檢"請求(preflight)。一般是一個OPTION請求。這個請求先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪http動詞和頭信息字段。只有獲得確定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,不然就報錯。

若是你們想要更詳細的瞭解CORS,能夠參考如下文章。

參考文章:

阮一峯《瀏覽器同源政策及其規避方法

阮一峯《跨域資源共享 CORS 詳解

參考書籍:

《javascript高級程序設計》

相關文章
相關標籤/搜索