跨域以及一些解決方法
跨域
最近在回顧一些知識,歸納一下以前的筆記再結合各個資料說一下我對跨域和跨域問題的解決方法。 產生跨域安全問題不是后台服務器不允許前台調用, 其本質是瀏覽器的同源策略(Same-origin policy)造成的,它是瀏覽器最基本和最核心的安全機制,同源是指URI scheme
、host name
、port number
相同,借用一下網上的栗子:
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 跨域,協議不同 復制代碼
如果非同源,將會受到如下限制:
- Cookie、LocalStorage 和 IndexDB 無法讀取。
- DOM 無法獲得。
- AJAX 請求不能發送。
瀏覽器發現前台代碼發出了一個非本域的請求,出於安全的考慮,瀏覽器會做一些校驗,如果校驗不通過,就無法完成這個請求,拋出請求跨域的錯誤

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
類型的,在日常開發中常用的請求類型還有POST
,PUT
,DELETE
,而JSONP只能發起GET
請求,是它的一大短板。
其次,JSONP是從其他域中加載代碼並執行,如果其他域不安全,很有可能會在執行的代碼中夾雜一些惡意代碼,所以在使用JSONP時一定要保證被請求方它安全可靠。
另外,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字段
- 不能訪問響應頭部信息
- 只支持
GET
和POST
請求
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-headers1
和f-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.com
和www.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://juejin.im/entry/5a967a6cf265da4e8b3010c6
參考資料
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高級程序設計(第三版)》