前端跨域問題的起因是什么—同源政策
1995年躺枕,同源政策由 Netscape 公司引入瀏覽器。目前供填,所有瀏覽器都實行這個政策拐云。
最初,它的含義是指近她,A網(wǎng)頁設置的 Cookie叉瘩,B網(wǎng)頁不能打開,除非這兩個網(wǎng)頁"同源"粘捎。所謂"同源"指的是"三個相同"薇缅。協(xié)議相同、域名相同攒磨、端口相同泳桦。
舉例來說
http://shaocx.com/index.html 這個網(wǎng)址協(xié)議是http://,域名是www.example.com娩缰,端口是80(默認端口可以省略)蓬痒。
它的同源情況如下。
http://shaocx.com/index2.html 同源
https://shaocx.com/index2.html 不同源(協(xié)議不同)
http://www.shaocx.com/index.html 不同源(域名不同)
http://shaocx.com:81/index.html 不同源(端口不同)
同源政策的目的
同源策略有助于保護使用經(jīng)過驗證會話的網(wǎng)站。下面是一個如果沒有同源政策會出現(xiàn)的例子梧奢。
假設用戶正在訪問一個銀行網(wǎng)站并且沒有注銷。然后用戶打開了一個有惡意JavaScript代碼的網(wǎng)站演痒,該網(wǎng)站向銀行網(wǎng)站請求數(shù)據(jù)亲轨。由于用戶沒有注銷銀行網(wǎng)站上的賬號,惡意代碼在銀行網(wǎng)站上做任意事情鸟顺。例如惦蚊,它可以獲取用戶最近一次交易的列表,或者創(chuàng)建一個新交易等讯嫂。
這主要是因為瀏覽器會根據(jù)請求的域名蹦锋,自動加上之前存儲在該域名底下的cookie。
而無意間訪問惡意網(wǎng)站的用戶欧芽,會期望他或她訪問的網(wǎng)站無法訪問銀行會話cookie莉掂。盡管JavaScript沒有直接訪問銀行會話cookie,但它仍然可以通過銀行網(wǎng)站的會話cookie向銀行網(wǎng)站發(fā)送和接收請求千扔。由于腳本基本上可以和用戶做的一樣憎妙,所以銀行網(wǎng)站的CSRF保護也不會有效。
由此可見曲楚,"同源政策"是必需的厘唾,否則 Cookie 可以共享,互聯(lián)網(wǎng)就毫無安全可言了龙誊。
同源政策限制范圍
1抚垃、 Cookie、LocalStorage 和 IndexDB 無法讀取趟大。
2鹤树、 DOM 無法獲得。
3护昧、 AJAX 請求不能發(fā)送魂迄。
4、 window對象無法獲取惋耙。
Cookie
Cookie 是服務器寫入瀏覽器的一小段信息捣炬,只有同源的網(wǎng)頁才能共享。但是绽榛,當兩個網(wǎng)頁一級域名相同湿酸,只是二級域名不同,瀏覽器允許通過設置document.domain共享 Cookie灭美。
1推溃、前端自己存取cookie時
A網(wǎng)頁是http://w1.shaocx.com/a.html
B網(wǎng)頁是http://w2.shaocx.com/b.html
document.cookie = "try1=1;domain=shaocx.com";
這樣cookie被設在頂級域名 .shaocx.com 上,兩個頁面都可以讀取到届腐。
2铁坎、后端添加cookie時
服務器也可以在設置Cookie的時候蜂奸,指定Cookie的所屬域名為一級域名,比如.shaocx.com硬萍。
Set-Cookie: key=value; domain=.shaocx.com; path=/
這里有一些地方需要注意:
1扩所、掛載在頂級域名上的是這種格式 .a.com ,前端在設置朴乖、書寫的時候祖屏,前面的 . 是不需要書寫的。但是在后端加上的時候买羞,這個 . 又是必須的袁勺。
2、cookie在讀取的時候畜普,只能讀取到key和value期丰。(path、host漠嵌、expires不能讀雀拦)
3、cookie在讀取時儒鹿,如果你在多個域名下都設置了化撕,那么會出現(xiàn)多個cookie。
4约炎、多個cookie時讀取的值會根據(jù)你使用的方法有所不同植阴。(我這邊的Cookies使用了js-cookie.js)
document.cookie會讀取到所有可以讀取到的cookie。而且是根據(jù)先后插入的順序進行讀取圾浅。
Cookies.get()會讀取先插入的值(根據(jù)document.cookie的順序)掠手。
Cookies.getJSON()會優(yōu)先讀取后插入的值(轉換成對象后值被覆蓋)。
iframe
iframe窗口和window.open方法打開的窗口一樣狸捕,如果不同源喷鸽,它們與父窗口無法通信。
先來回顧一下iframe之間互相通信的方法灸拍。
父子iframe互相操作
子iframe獲取父iframe做祝,parent.window,top.window
父iframe獲取子iframe鸡岗,document.getElementById("a").contentWindow
跨域會存在以下問題
window對象無法讀取具體的對象值混槐,只能讀取系統(tǒng)默認方法。
window.location 可以設置轩性,但不能讀取声登。其它的 location 屬性和方法被禁止訪問;
document 不可以設置,也不能讀取悯嗓。
<iframe> 的 src 可以設置件舵,也可以讀取。但是在iframe中將自己的地址改變之后绅作,iframe的src地址并不會變更芦圾。
iframe - document.domain
如果兩個窗口根域名相同,那么設置上面介紹的document.domain屬性俄认,就可以規(guī)避同源政策,互相操作洪乍。
iframe - location.hash
location.hash是網(wǎng)頁中#后面的部分眯杏,如果只是改變hash值,頁面不會重新刷新壳澳。
// 父頁面可以把信息寫入已知地址的iframe的hash中岂贩。
document.getElementById('a').src = url + '#' + data;
// iframe中通過監(jiān)聽hashchange事件收到信息。
window.onhashchange = function () {
var message = window.location.hash;
}
// iframe改變父頁面的hash地址
parent.location.href = url + "#" + data;
iframe - window.name
window一個name屬性巷波,它最大特點是萎津,無論是否同源都可以讀取它。
父窗口先打開一個子窗口抹镊,載入一個不同源的網(wǎng)頁锉屈,該網(wǎng)頁將信息寫入window.name屬性洲押。
window.name = data;
接著宪拥,子窗口跳回一個與主窗口同域的網(wǎng)址缸废。
location = ‘http://parent.url.com/xxx.html';
然后馆里,主窗口就可以讀取子窗口的window.name了娃兽。
var data = document.getElementById('myFrame').contentWindow.name;
這種方法的優(yōu)點是吭历,window.name容量很大墨辛,可以放置非常長的字符串英融;缺點是必須監(jiān)聽子窗口window.name屬性的變化铃彰,影響網(wǎng)頁性能绍豁。
// iframe頁面
window.name = 'I was there!'; // 這里是要傳輸?shù)臄?shù)據(jù),大小一般為2M牙捉,IE和firefox下可以大至32M左右
// 數(shù)據(jù)格式可以自定義竹揍,如json、字符串
// 父頁面
var state = 0,
iframe = document.createElement('iframe'),
loadfn = function() {
if (state === 1) {
var data = iframe.contentWindow.name; // 讀取數(shù)據(jù)
alert(data); //彈出'I was there!'
} else if (state === 0) {
state = 1;
iframe.contentWindow.location = "http://a.com/proxy.html"; // 設置的代理文件
}
};
iframe.src = 'http://b.com/data.html';
iframe.onload = loadfn;
document.body.appendChild(iframe);
iframe - postMessage
上面兩種方法都屬于破解鹃共,HTML5為了解決這個問題鬼佣,引入了一個全新的API:跨文檔通信 API(Cross-document messaging)。
這個API為window對象新增了一個window.postMessage方法霜浴,允許跨窗口通信晶衷,不論這兩個窗口是否同源。這個方法可用在與window.open打開的新頁面進行通訊,或者與iframe中的頁面進行通訊晌纫。
// 這是iframe向父頁面窗口發(fā)送消息税迷。
window.parent.postMessage('Hello World!', 'http://bbb.com');
// 這是父頁面向iframe窗口發(fā)送消息。
document.getElementById("a").contentWindow.postMessage('Nice to see you', 'http://aaa.com');
postMessage方法的第一個參數(shù)是string格式的具體的信息內容锹漱,第二個參數(shù)是接收消息的窗口的源(origin)箭养,即"協(xié)議 + 域名 + 端口",設置該值后哥牍,只會向固定域名的頁面發(fā)送信息毕泌,使用其他域名窗口打開當前iframe時,不會發(fā)送消息嗅辣。也可以設為*撼泛,表示不限制域名,向所有窗口發(fā)送澡谭。
父窗口和子窗口都可以通過message事件愿题,監(jiān)聽對方的消息。
window.addEventListener('message', function(e) {
// data是數(shù)據(jù)蛙奖,origin是域名潘酗。這邊最好也判斷一下,可以提升安全性
console.log(e.data, e.origin);
});
localStorage
document.domain這種方法只適用于 Cookie 和 iframe 窗口雁仲,LocalStorage 無法通過這種方法仔夺,而要使用PostMessage API。
現(xiàn)有方案大多為使用postMessage與iframe組合來傳遞localStorage伯顶。
父頁面監(jiān)聽子頁面?zhèn)鱽淼膁ata囚灼,然后渲染到localStorage中。再子頁面監(jiān)聽父頁面?zhèn)鱽淼膁ata祭衩,渲染到自己的localStorage中灶体。
// 這是父頁面發(fā)送消息的代碼
var data = JSON.stringify({key: 'storage', data: {name: ‘Jack’}});
document.getElementsByTagName('iframe')[0].contentWindow.postMessage(data, 'http://bbb.com');
// 這是子頁面監(jiān)聽父頁面發(fā)來的消息,并放置在localStorage中掐暮。
window.onmessage = function(e) {
var payload = JSON.parse(e.data);
localStorage.setItem(payload.key, JSON.stringify(payload.data));
};
子頁面發(fā)送至父頁面也同理蝎抽。
AJAX
AJAX - 架設服務器代理
架設服務器代理(瀏覽器請求同源服務器,再由后者請求外部服務)
比如nginx的反向代理可以將請求直接轉發(fā)出去路克,并且接收返回值樟结。
AJAX - JSONP
JSONP是服務器與客戶端跨源通信的常用方法。最大特點就是簡單適用精算,老式瀏覽器全部支持瓢宦,服務器改造非常小。
它的基本思想是灰羽,網(wǎng)頁通過添加一個<script>元素驮履,向服務器請求JSON數(shù)據(jù)鱼辙,這種做法不受同源政策限制;服務器收到請求后玫镐,將數(shù)據(jù)放在一個指定名字的回調函數(shù)里傳回來倒戏。
首先,網(wǎng)頁動態(tài)插入<script>元素恐似,由它向跨源網(wǎng)址發(fā)出請求杜跷。
function addScriptTag(src) {
var script = document.createElement('script');
script.setAttribute("type","text/javascript");
script.src = src;
document.body.appendChild(script);
}
window.onload = function () {
addScriptTag('http://a.com/ip?callback=foo');
}
function foo(data) {
console.log('Your public IP address is: ' + data.ip);
};
上面代碼通過動態(tài)添加<script>元素,向服務器a.com發(fā)出請求矫夷。注意葛闷,該請求的查詢字符串有一個callback參數(shù),用來指定回調函數(shù)的名字双藕,這對于JSONP是必需的孵运。服務器收到這個請求以后,會將數(shù)據(jù)放在回調函數(shù)的參數(shù)位置返回蔓彩。
// 服務器
foo({"ip": "8.8.8.8"});
由于<script>元素請求的腳本,直接作為代碼運行驳概。這時赤嚼,只要瀏覽器定義了foo函數(shù),該函數(shù)就會立即調用顺又。
AJAX - WebSocket
WebSocket 協(xié)議在2008年誕生更卒,2011年成為國際標準。所有瀏覽器都已經(jīng)支持了稚照。
它的最大特點就是蹂空,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發(fā)送信息果录,是真正的雙向平等對話上枕,屬于服務器推送技術的一種。
但是嚴格地說弱恒,WebSocket技術不屬于HTML5辨萍,這個技術是對HTTP無狀態(tài)連接的一種革新,本質就是一種持久性socket連接返弹,在瀏覽器客戶端通過javascript進行初始化連接后锈玉,就可以監(jiān)聽相關的事件和調用socket方法來對服務器的消息進行讀寫操作。與Ajax相比义起,Ajax技術需要客戶端發(fā)起請求拉背,而WebSocket服務器和客戶端可以彼此相互推送信息;XHR受到域的限制默终,而WebSocket允許跨域通信椅棺,這個特性導致我們至少可以用來做遠控犁罩。
WebSocket實現(xiàn)了全雙工通信,使WEB上的真正的實時通信成為可能土陪。瀏覽器和服務器只需要做一個握手的動作昼汗,然后,瀏覽器和服務器之間就形成了一條快速通道鬼雀。兩者之間就直接可以數(shù)據(jù)互相傳送顷窒。在WebSocket協(xié)議中,為我們實現(xiàn)即時服務帶來了三個好處:
1源哩、客戶端和服務器端之間數(shù)據(jù)傳輸時請求頭信息比較小,大概2個字節(jié)鞋吉。
2、服務器和客戶端可以相互主動的發(fā)送數(shù)據(jù)給對方励烦。
3谓着、不需要多次創(chuàng)建TCP請求和銷毀,節(jié)約寬帶和服務器的資源坛掠。
具體可以參考
WebSocket 教程 - 阮一峰
跨域(二)——WebSocket
AJAX - CORS
CORS是跨源資源分享(Cross-Origin Resource Sharing)的縮寫赊锚。它是W3C標準,是跨源AJAX請求的根本解決方法屉栓。相比JSONP只能發(fā)GET請求舷蒲,CORS允許任何類型的請求。
CORS需要瀏覽器和服務器同時支持友多。目前牲平,所有瀏覽器都支持該功能,IE瀏覽器不能低于IE10域滥。
整個CORS通信過程纵柿,都是瀏覽器自動完成,不需要用戶參與启绰。對于開發(fā)者來說昂儒,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣酬土。瀏覽器一旦發(fā)現(xiàn)AJAX請求跨源荆忍,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求撤缴,但用戶不會有感覺刹枉。
因此,實現(xiàn)CORS通信的關鍵是服務器屈呕。只要服務器實現(xiàn)了CORS接口微宝,就可以跨源通信。
具體可以參考 跨域資源共享 CORS 詳解 - 阮一峰