跨域以及一些解決方法

跨域

最近在回顧一些知識,概括一下之前的筆記再結合各個資料說一下我對跨域和跨域問題的解決方法。 產生跨域安全問題不是後臺服務器不容許前臺調用, 其本質是瀏覽器的同源策略(Same-origin policy)形成的,它是瀏覽器最基本和最核心的安全機制,同源是指URI schemehost nameport number相同,借用一下網上的栗子:php

http://www.bear.cn/index.html 調用 http://www.bear.cn/server.php 非跨域

http://www.bear.cn/index.html 調用 http://www.jasmine .cn/server.php 跨域,主域不一樣

http://gogo.bear.cn/index.html 調用 http://ge.jasmine.cn/server.php 跨域,子域名不一樣

http://www.bear.cn:2018/index.html 調用 http://www.bear.cn/server.php 跨域,端口不一樣

https://www.bear.cn/index.html 調用 http://www.bear.cn/server.php 跨域,協議不一樣
複製代碼

若是非同源,將會受到以下限制:html

  • Cookie、LocalStorage 和 IndexDB 沒法讀取。
  • DOM 沒法得到。
  • AJAX 請求不能發送。

瀏覽器發現前臺代碼發出了一個非本域的請求,出於安全的考慮,瀏覽器會作一些校驗,若是校驗不經過,就沒法完成這個請求,拋出請求跨域的錯誤 json

Jsonp

JSONP是JSON with padding(填充式JSON或參數式JSON)的簡寫,是應用JSON的一種辦法,JSONP看起來和JSON差很少,只不過是被包含在函數調用中的JSON,就像這樣:後端

callback({"name": "Nicholas Bear"})
複製代碼

JSONP由兩部分組成:回調函數和數據。回調函數是當瀏覽器接收到響應時調用的函數,回電函數名通常在請求中指定,數據就是回調函數的參數。以下就是典型的JSONP請求:跨域

http://somewhere-else/json/?callback=handleResponse
複製代碼

這裏指定的回調函數就是handleResponse()瀏覽器

JSONP實現原理是經過JS腳本動態生成一個script元素,爲其src屬性指定一個跨域URL,這裏的script元素和img、link元素相似,都有能力不受限制地從其餘域加載資源。它並非官方的協議,而是一種hack手段,看一個簡單的栗子:緩存

function handleResponse(res) {
    alert("got message", res);
}
var script = document.createElement("script"),
    body = document.body;
script.src = "http://somewhere-else/json/?callback=handleResponse";
body.insertBefore(script, body.firstChild);
複製代碼

JSONP實現跨域訪問很是方便,簡單易用,可是也有不足的地方:安全

首先,從它的實現方式能夠看出來,它是發起一個資源獲取請求,是GET類型的,在平常開發中經常使用的請求類型還有POSTPUTDELETE,而JSONP只能發起GET請求,是它的一大短板。服務器

其次,JSONP是從其餘域中加載代碼並執行,若是其餘域不安全,頗有可能會在執行的代碼中夾雜一些惡意代碼,因此在使用JSONP時必定要保證被請求方它安全可靠。cookie

另外,JSON和JSONP還有一個區別須要特別注意,JSONP請求返回來的不是JSON數據,而是一個JavaScript腳本,爲了實現JSONP跨域,須要後臺服務器配合。

最後,因爲它的請求類型並非XHR,就缺乏了一些事件處理程序,要追蹤JSONP請求是否失敗並不容易,或者爲JSONP請求增長定時器,超時就視爲請求失敗,接下來就再次發送請求或者作其餘事情,可是每一個用戶的網絡情況並不能保證,這樣作也不是萬全之策。

CORS

CORS(Cross-origin resource sharing)跨域源資源共享,是W3C的一個工做草案, 定義了在跨域訪問時,瀏覽器與服務器的溝通方式,具體實現爲,使用自定義的HTTP頭部讓瀏覽器與服務器進行溝通,從而決定跨域請求或響應時應該成功,仍是應該失敗。

好比說發起一個GET跨域請求,Content-type是text/plain,在發送跨域請求前,瀏覽器會爲http頭部加上一個額外的Origin頭部,其中包含了頁面的源信息(協議、域名和端口號),這個額外的Origin決定了服務器是否響應該請求。一個Origin頭部實例:

Origin: https://www.somewhere-else.net
複製代碼

若是服務器承認該請求就會在響應頭加上Access-Control-Allow-Origin標誌字段,值能夠是與請求頭帶來的Origin相同,若是該服務器上的是公共資源,值就是「*」。

Access-Control-Allow-Origin: https://www.somewhere-else.net
複製代碼

若是響應頭中沒有這個這個字段,說明服務器拒絕了此次跨域請求,會拋出一個錯誤,可是並不能被xhr的onerror事件捕獲。默認狀況下跨域請求都是不帶憑證的(cookie,HTTP認證及服務端SSL證實等),經過修改xhr對象的withCredentials(IE10之前的版本不支持該屬性)設置爲true,能夠指定某個請求攜帶憑證。若是服務器容許跨域請求攜帶憑證響應頭部會有標示。

Access-Control-Allow-Credentials: true
複製代碼

若是發送的是帶憑證的請求,響應頭裏卻沒有這個字段,那麼瀏覽器就不會吧響應交給JS,意思是xhr獲取到的responseText爲空,status爲0,這個時候onerror能夠捕獲到該錯誤.

XHR對象在跨域時也是有限制的:

  • 不能使用setRequestHeader()來設置頭部
  • 默認狀況下沒法發送cookie
  • 調用getAllResponseHeaders()方法總會返回空字符串

CORS的實現:

var xhr = new XMLHttpRequest();
xhr.onreadystateChange = function() {
    if(xhr.readyState === 4) {
        if(xhr.status >= 200 && xhr.status <= 300 || xhr.status === 304) {
            alert(xhr.responseText);
        } else {
            alert("error ", xhr.status);
        }
    }
}
xhr.open("get", "http://www.somewhere-else.com/page", true);
xhr.send(null);
複製代碼

發送CORS請求和發送普通的xhr對象差異不大, 只須要在地址處寫絕對地址便可.跨域所須要作的工做就交給瀏覽器,對於用戶來講是透明.

IE瀏覽器是用XDR(XDomainRequest)來實現CORS的,它和XHR類似,可是能提供能安全可靠的跨域通訊:

  • cookie不會隨請求發送,也不會隨響應返回
  • 只能設置請求頭部信息中的Content-Type字段
  • 不能訪問響應頭部信息
  • 只支持GETPOST請求

XDR對象和xhr的使用方法類型,也是創造一個XDomainRequest的實例,調用open()方法,再調用send()方法,可是與xhr對象的open()不一樣,XDR對象的open()方法只接受兩個參數:請求的類型和URL,XDR發送的請求都是異步執行的。並且XDR對象沒法訪問status屬性,因此在使用XDR時必定得經過onerror事件處理程序來捕獲錯誤.

簡單請求

跨域請求在發送前,瀏覽器會檢查這個請求是否是簡單請求,簡單請求知足下面兩個條件:

  • 請求方式爲HEAD,POST,GET
  • HTTP頭部信息包括但不超過如下字段
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type(application/x-www-form-urlencode,multipart/form-data,text/plain)

若是知足這些條件,瀏覽器就會在請求頭部增長額外的Origin字段後發送跨域請求。

響應頭通常包含這些字段:

  • Access-Control-Allow-Origin,若是瀏覽器校驗經過,這個字段顯示的是請求頭的Origin值或者*
  • Access-Control-Allow-Credential,值爲布爾型,表示請求頭是否能夠攜帶cookie
  • Access-Control-Expose-Headers。拓展的頭部信息,瀏覽器將CORS響應交給JS後,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其餘字段,就必須在Access-Control-Expose-Headers裏面指定。

注意,若是你想在請求中攜帶憑證,上面已經說過了,必須將xhr的withCrediential屬性設置爲true,但有時會報錯,錯誤信息以下圖:

錯誤提示裏說若是想要在請求頭中攜帶憑證,那麼響應頭中的 Access-Control-Allow-Origin必須和請求頭中的Origin一致,而不能是「*」,解決方法很簡單,修改一下後端代碼就能夠了。

非簡單請求

CORS經過一種叫作Preflighted Requestes預請求的透明服務器驗證機制支持開發人員使用自定義的頭部,GET和POST以外的方法,以及不一樣類型的主題內容。也就是說想要發送這種非簡單的跨域請求之前會先發送一個詢問請求(攜帶非簡單請求部分信息)來詢問服務器是否贊成此次非簡單請求,這種詢問請求使用OPTIONS方法,發送如下頭部:

  • Origin:和簡單請求相同
  • Access-Control-Request-Method:請求自身使用的方法
  • Access-Control-Request-Headers:這是一個可選頭部字段,多個頭部以逗號分開。

發送這個請求之後,服務器能夠決定是否容許這種類型的請求。服務器能夠經過在響應頭中攜帶如下頭部與瀏覽器溝通:

  • Access-Control-Allow-Origin:和簡單請求相同
  • Access-Control-Allow-Methods:容許的方法
  • Access-Control-Allow-Headers: 容許的頭部
  • Access-Control-Max-Age: 預請求的有效期或者緩存存活時間(秒)

好比說我如今發送了一個自定義頭部字段f-headers1f-headers2,方法爲post的非簡單請求,那麼首先發送的預請求頭部會包含如下信息:

Origin: http://www.yourhostname.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: f-headers1, f-headers2
複製代碼

若是服務器容許這樣的非簡單請求的跨域訪問,返回的響應頭會包含這些字段:

Access-Control-Allow-Origin: http://www.yourhostname.com
Access-Control-Allow-Method: POST,GET,PUT,DELETE
Access-Control-Allow-Headers: f-headers1, f-headers2
Access-Control-Max-Age: 3600
複製代碼

預請求結束後,結果將按照響應中指定的時間緩存起來,下次再發送這樣的非簡單請求以前就不會再發送詢問請求.

Cookie

上述幾條都是解決跨域請求資源,可是若是想要獲取非同源的cookie,LocalStorage或IndexDB怎麼辦。cookie是服務器在瀏覽器上寫下的一小段認證信息,大小通常是4k,根據瀏覽器的不一樣,每一個域容許種下的cookie數量也不一樣。cookie只有在同源的域下才能共享,可是咱們能夠經過修改document.domain來共享cookie,以下所示

// a.abc.com
document.domain = "abc.com";
document.cookie = "name=bingo";
// b.abc.com
document.domain = "abc.com";
console.log(document.cookie); // "name=bingo"
複製代碼

可是這種方法前提是這兩個網頁一級域名相同,一級域名或者叫根域名相同是什麼意思呢,好比說這裏有個兩個域名www.abc.comwww.f.abc.com它們的一級域名都是abc.com。二級域名就是增長了一級包括www,好比說www.zdt.com,netgo.ccdn.com,www.baidu.com等等.三級,四級域名同理.

並且這種方法只適用於cookie和iframe.沒法獲取locastorage和IndexDB.

iframe

利用iframe解決跨域問題也是一種可取的辦法.光是給iframe增長src獲取其餘頁面的資源是不現實,必須藉助一些特性實現hack手段.

document.domain

兩個iframe之間或者父窗口和子窗口之間。如上述例子裏經過改變相同主域的document.domain能夠跨域獲取cookie,也能夠獲取對方的全局變量。這種方法和跨域獲取cookie同樣,只適合具備相同主域的跨域訪問。實現原理爲相同主域的網站設置相同的document.domain,瀏覽器就職務它們是同源的,這種方式比較簡單,但也有安全問題,若是某一個網站被攻擊後,另外一個網站就會有安全漏洞

window.name

window.name,它具備更新了頁面的location更新後,值依然不會更變的神奇特性,這讓咱們跨域訪問信息提供了機會。在一個頁面中建立一個不一樣域的iframe,這個iframe的js代碼修改它window.name的值,而後再將它變爲和父窗口同域的iframe,在父窗口中就能夠經過iframe得到修改事後的window.name的值

location.hash

location.hash又稱片斷標識符(Fragment Identitier),它是URL字符中#後面的部分,好比http://www.somewhere-else.com/a.html#fragment,這裏的片斷標識符就是fragment,URL中的片斷標識符改變並不會引發頁面刷新.利用location.hash實現跨域訪問信息的原理是父窗口能夠讀寫子窗口的URL,子窗口只能讀寫相同域父窗口的URL.這裏想要實現跨域,不一樣域的子窗口就必須藉助一個與父窗口同域的代理. 舉個栗子

a.abc.com/index.html(a)下有一個src爲smg.com/index.html(b)的iframe.

1.a頁面給b頁面發送數據

  • a修改b的src爲smg.com/index.html#data
  • b頁面訪問本身的location.hash便可拿到數據

2.b頁面給a頁面發送數據,b因爲不能修改不一樣域父窗口的URL,因此b頁面須要動態建立一個和父窗口同域的iframe來作代理.

  • b頁面建立一個src爲a.abc.com/proxy.html#data的子窗口
  • 這個proxy頁面經過onhashchange(兼容狀況)事件監聽本身href的變化,事件觸發後經過修改a頁面的hash來達到傳遞數據的功能
  • a頁面訪問本身的location.hash便可拿到數據

postMessage

不論是iframe和location.hash、document.domain仍是window.name都是屬於非官方的跨越方法,下面要介紹的就是一個官方方法---postMessage,它是HTML5新增的一個跨文檔通訊API,它實現了即便不一樣域也能夠跨窗口直接通訊的功能,並且只要使用得當,這種方法就很安全。

調用對象爲父窗口或者的window對象、window.open()的返回值或者是iframe的contentWindow這個屬性,這個方法接受兩個參數,第一個是要發送的消息,第二個參數是指定接受消息的接收源,能夠是*表示全部窗口均可以接收到消息或者是一個url,但只有在協議,域名和端口號都相同纔會接收到消息。

添加如下代碼便可接收

window.addEventListener("message", receiveMessage, false);
function receiveMessage(event) {
  // For Chrome, the origin property is in the event.originalEvent
  // object.
  var origin = event.origin || event.originalEvent.origin; 
  if (origin !== "http://example.org:8080")
    return;

  // ...
}
複製代碼

事件event對象有三個屬性

  • data,發送過來的信息
  • origin,發送發窗口的origin
  • source,對發送消息的窗口對象的引用; 您可使用此來在具備不一樣origin的兩個窗口之間創建雙向通訊

具體實例看這裏,阮一峯老師的點擊這裏

參考資料

https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

http://www.ruanyifeng.com/blog/2016/04/cors.html

http://blog.csdn.net/kongjiea/article/details/44201021

《JavsScript高級程序設計(第三版)》

相關文章
相關標籤/搜索