摘要:同源策略年,同源政策由公司引入瀏覽器。標簽不受同源策略限制,但只能發起請求。這一行為使得不同域的特定文檔可以讀取該屬性值,因此可以繞過同源策略并使跨域消息通信成為可能。
前言
現在cross-origin resource sharing(跨域資源共享,下簡稱CORS)已經十分普及,算上IE8的不標準兼容(XDomainRequest),各大瀏覽器基本都已支持,當年為了前后端分離、iframe交互和第三方插件開發而頭疼跨域是時代已經過去,但當年為了跨域無所不用其極的風騷操作卻依然值得學習。
本篇文章不是從實用的角度考量這些舊時代的跨域手段,而是更偏向理論的闡述,并引發對瀏覽器安全的思考,因為跨域實際上也是各類攻擊的核心。
本人個人能力有限,歡迎大牛一起討論,批評指正。
1995年,同源政策由Netscape公司引入瀏覽器。目前,所有瀏覽器都實行這個安全策略。
核心是確保不同源提供的文件(資源)之間是相互獨立的。換句話說,只有當不同的文件腳本是由相同的域、端口、HTTP協議提供時,才沒有特殊的限制。特殊限制可以細分為兩個方面:
對象訪問限制:主要體現在iframe,如果父子頁面的源是不同的,那就不可以訪問對方的DOM方法和屬性(包括Cookie、LocalStorage和IndexDB等)。不同來源便拋出異常。
網絡訪問限制:主要體現在AJAX請求,如果發起的請求目標源與當前頁面不同,瀏覽器就會限制了發起跨站請求,或攔截返回的請求。
一個表格看懂什么是同源?
origin(URL) | result | reason |
---|---|---|
http://example.com | success | 協議,域名和端口號80均相同 |
http://example.com:8080 | fail | 端口不同 |
https://example.com | fail | 協議不同 |
http://sub.example.com | fail | 域名不同 |
至于為什么說這是個安全策略?
這個就要提到cookie-session機制,眾所周知HTTP是無狀態協議,而服務器如何知曉用戶的登錄狀態?傳統上是使用了cookie-session這一機制,也就是服務器為每個訪問者生成了一個session標識,而session標識會被服務器包含在應答頭中返回,瀏覽器解析到應答頭中的set-cookie就把這串session標識保存到本地cookie中,利用cookie每次請求同一個域都會帶上的特性,服務器器就能知曉當前的用戶登錄狀態。
所以如果讓瀏覽器向不同源發起請求,就會造成很大的危險。比如用戶登錄了銀行的網站A,也就是說A站已經在瀏覽器留下了cookie,這時候用戶又訪問了B站,如果能在B站頁面上發起A站的請求,就相當于B站可以冒充用戶,在A站為所欲為。
由此可見,"同源策略"是必需的,否則cookie可以共享,互聯網就毫無安全可言了。
同源策略提出的時代還是傳統MVC架構(jsp,asp)盛行的年代,那時候的頁面靠服務器渲染完成了大部分填充,內容也比較簡單,開發者也不會維護獨立的API工程,所以其實跨域的需求是比較少的。
新時代前后端的分離和第三方JSSDK的興起,我們才開始發現這個策略雖然大大提高了瀏覽器的安全性,但有時很不方便,合理的用途也受到影響。比如:
獨立的API工程部署為了方便管理使用了獨立的域名;
前端開發者本地調試需要使用遠程的API;
第三方開發的JSSDK需要嵌入到別人的頁面中使用;
公共平臺的開放API。
于是乎,在沒有標準規范的時代,如何解決這些問題的跨域方案就被紛紛提出,可謂百家爭鳴,其中不乏令人驚嘆的騷操作,這樣的極客精神依然值得我們敬佩和學習。
JSON-PJSON-P是各類跨域方案中流行度較高的一個,現在在某些要兼容舊瀏覽器的環境下還會被使用,著名的jQuery也封裝其方法。請勿見名知義,名字中的P是padding“帶填充”的意思,這個方法在通信過程中使用的并不是普通的json,而是自帶填充功能的JavaScript腳本。
如何理解“自帶填充功能的JavaScript腳本”,看看下面的例子或許比較簡單,如果一個js文件里這樣寫并被引入,則全局下就會有data對象,也就是說利用js腳本的引入和解析可以用來傳遞數據,如果把js腳本換成函數運行命令豈不是可以調用全局函數了。這就是JSON-P方法的核心思想,它填充的是全局函數的數據。
var data = { a: 1, b: 2 }
【PS】標簽不受同源策略限制,但只能發起get請求。
原理及流程
先定義好回調函數,也就是引入的js腳本中要調用的函數;
新建標簽,將標簽插入頁面瀏覽器便會發起get請求;
服務器根據請求返回js腳本,其中調用了回調函數。
// 定義回調函數 function getTheAnimal(data){ var myAnimal = data.animal; } // 新建標簽 var script = document.createElement("script"); script.type = "text/javascript"; // 常用的在url參數部分跟服務器約定號回調函數名 script.src = "http://demo.com/animal.json?callback=getTheAnimal"; document.getElementByTagName("head")[0].appendChild(script);
總結
優點:
簡單,有現成的工具庫(jQuery)支持;
支持上古級別的瀏覽器(IE8-)。
缺點:
只能是GET方法;
受瀏覽器URL最大長度2083字符限制;
無法調試,服務器錯誤無法檢測到具體原因;
有CSRF的安全風險;
只能是異步,無法同步阻塞;
需要特殊接口支持,不能基于REST的API規范。
子域名代理這個方法實際上是利用瀏覽器允許iframe內的頁面只要是跟父頁面是同個一級域名下,就能被父頁面修改和調用的特點。也許你會疑問,上面講同源策略的表格中很明確二級域名不同也是算不同源,這豈不矛盾了?
這其實不矛盾,如果正常操作確實會被同源策略限制,但瀏覽器的document.domain允許網站將主機部分更改為原始值的后綴。這意味著,寄放在sub.example.com的頁面可以將它的源設置為example.com,但是并不能將其設置為alt.example.com或google.com。
【PS】這里有一個細節,父子頁面均要設置document.domain才能被互相訪問,單一一個是無法跨域的。document.domain的特點:只能設置一次;只能更改域名部分,不能修改端口號和協議;重置源的端口為協議默認端口。
原理及流程
新建一個子域,比如api.demo.com(頁面在主域名demo.com下);
子域下需要一個代理文件proxy.html,設置其document.domain = "demo.com",并可以包含發起ajax的工具;
所有API地址都是在api.demo.com;
把需要發請求的主域頁面設置其document.domain = "demo.com";
新建iframe標簽鏈接到代理頁;
當iframe內的子頁面就緒時,父頁面就可以使用子頁面發起ajax請求。
// 最簡單的代理文件proxy.html
// 新建iframe var iframe = document.createElement("iframe"); // 鏈接到代理頁 iframe.src = "http://api.demo.com/proxy.html"; // 代理頁就緒時觸發 iframe.onload = function(){ // 由于代理頁已經和父頁設置了相同的源,父的腳本可以調用代理頁的ajax工具; // 由于是在子頁面發起,其請求地址就跟子頁面同源了。 iframe.contentWindow.jQuery.ajax({ method: "POST", url: "http://api.demo.com/products", data: { product: id, }, success: function(){ document.body.removeChild(iframe); /*...*/ } }) } document.getElementsByTagName("head")[0].appendChild(iframe);
總結
優點:
可以發送任意類型的請求;
可以使用基于REST的API規范。
缺點:
不太適合第三方API,給第二方使用較麻煩;
iframe對瀏覽器性能影響較大;
無法使用非協議默認端口的API。
模擬form表單form表單的target屬性可以指定一個iframe,使主頁面不跳轉,而iframe內跳轉,所以這個方法的核心就是利用表單提交,并在iframe中獲取數據。
要訪問iframe內外頁面互訪也是必須設置同源,這點與子域代理是相似的;而iframe內回調父頁面,又與JSON-P相似,可以說是兩個思路的合體版。
form表單提交后返回的是頁面,所以與JSON-P不同的是,返回的是包含了自帶填充功能的JavaScript腳本的頁面,說起來有點繞,簡單來說就是把JSON-P返回的腳本放到一個html頁面里自運行。
相比子域代理的方法,它不需要代理頁。
【PS】form表單提交的特點就是會導致整個頁面跳轉,返回數據是在新的頁面上,這樣自然不會產生跨域的問題。
原理及流程
新建一個子域,比如api.demo.com(頁面在主域名demo.com下);
所有API地址都是在api.demo.com;
把需要發請求的主域頁面設置其document.domain = "demo.com";
先定義好父頁面上的回調函數;
新建iframe標簽并指定名字;
新建表單form標簽,指定target為剛才的iframe,并添加數據;
提交表單,iframe內跳轉,其中自運行腳本調用了父頁面的回調函數。
// 新建并隱藏iframe var frame = document.createElement("iframe"); iframe.name = "post-review"; frame.style.display = "none"; // 新建表單 var form = document.createElement("form"); form.action = "http://api.demo.com/products"; form.method = "POST"; form.target = "post-review"; // 添加數據 var score = document.createElement("input"); score.name = "score"; score.value = "5"; // 添加數據 var message = document.createElement("input"); message.name = "message"; message.value = "hello world"; // 把數據加到表單 form.appendChild(score); form.appendChild(message); // 渲染iframe和表單 document.body.appendChild(frame); document.body.appendChild(form); // 提交表單發起請求 form.submit(); // 完成清理元素 document.body.removeChild(form); document.body.removeChild(frame);
// 最簡單返回html
總結
由于這個方法是JSON-P與子域名代理的結合版,可以說即擁有兩者的優點,也保留了兩者一些缺點。
優點:
可以發送任意類型的請求;
不需要代理頁;
支持上古級別的瀏覽器(IE8-)。
缺點:
不太適合第三方API,給第二方使用較麻煩;
iframe對瀏覽器性能影響較大;
無法使用非協議默認端口的API;
需要特殊接口支持,不能基于REST的API規范。
window.name這方法利用了window.name的特性:一旦被賦值后,當窗口被重定向到一個新的URL時不會改變它的值。這一行為使得不同域的特定文檔可以讀取該屬性值,因此可以繞過同源策略并使跨域消息通信成為可能。
【PS】例子里演示的是發起get請求,只要把請求地址直接寫到src里就行了。如果想要發起其他類型的請求,可以類比采用模擬的form的方式進行改造。
原理及流程
新建iframe,使用iframe訪問一個非同源的地址(發請求);
當頁面加載完成后,iframe內腳本給window.name屬性賦值,這時父頁面還是不能讀取到子頁面的屬性(因為不同源);
iframe自身回調到一個同源的地址(可能只是個空白頁),這時候window.name沒有改變;
父頁面順利讀取window.name的值。
// 新建iframe var iframe = document.createElement("iframe"); var body = document.getElementByTagName("body"); // 隱藏iframe并鏈接地址 iframe.style.display = "none"; iframe.src = "http://api.demo.com/server.html?id=1"; // 因為需要兩次跳轉,這里有個完成標記 var done = fasle; // 這里會觸發至少兩次,一次由于非同源是取不到值的。 iframe.onload = iframe.onreadystatechange = function(){ if(! this.readyState && (iframe.readyState !== "complete" || done)){ return; } console.log("Listening"); var name = iframe.contentWindow.name; if(name){ console.log(iframe.contentWindow.name); done = true; } }; body.appendChild(iframe);
// 最簡單返回html
總結
優點:
可以發送任意類型的請求;
不需要設置子域名。
缺點:
iframe對瀏覽器性能影響較大;
需要特殊接口支持,不能基于REST的API規范;
每當你想要獲取一條新的消息時都不得不發起兩次網絡請求,網絡成本大;
需要準備空白頁,對它的訪問是無意義的,影響流量統計。
window.hash這個方法利用了location的特性:不同域的頁面,可以寫不可讀。而只改變哈希部分(井號后面)不會導致頁面跳轉。也就是可以讓父、子頁面互相寫對方的location的哈希部分,進行通訊。
原理及流程
新建iframe,使用iframe訪問一個非同源的地址(發請求),參數里帶上父頁面url;
當頁面加載完成后,iframe內腳本設置父頁面的url并在哈希部分帶上數據;
父頁面的腳本循環檢查哈希值的變化,如果檢查到有值就取值并清空哈希值;
【PS】父頁面會循環檢查哈希是否改變來讀取值,因為這種降級方案的使用環境一般是不會有hashchange事件的。演示里是最簡單的get方法,如果想要發起其他類型的請求,可以類比采用模擬的form的方式進行改造,但記住不要丟失父頁面的url。
// 獲取當前url var url = window.location.href; // 新建iframe var iframe = document.createElement("iframe"); // 隱藏iframe并設置鏈接,把當前url帶上 iframe.style.display = "none"; iframe.src = "http://api.demo.com/server.html?id=1&url=" + encodeURIComponent(url); var body = document.getElementByTagName("body")[0]; body.appendChild(iframe); // 循環監聽處理 var listener = function(){ // 讀取 var hash = location.hash; // 還原 if(hash && hash !== "#"){ console.log(hash.replace("#", "")); window.loacation.href = url + "#"; } // 繼續監聽 setTimeout(listener, 100); }; listener();
// 最簡單返回html
總結
優點:
可以發送任意類型的請求;
不需要設置子域名。
缺點:
iframe對瀏覽器性能影響較大;
需要特殊接口支持,不能基于REST的API規范;
循環檢查哈希需要消耗性能;
返回數據受瀏覽器URL最大長度2083字符限制。
現代的標準W3C的標準化跨域方案,讓現代瀏覽器跨域已經不是什么復雜的事。這部分網上資料已經很多,這里就只是簡單介紹。
CORSCORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。
它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
CORS參考文檔
跨域資源共享 CORS 詳解
H5的window.postMessage為瀏覽器帶來了一個安全的。基于事件的消息api。
只要是window對象,基本都可以使用這個方法,也就是說window.name、window.hash這類風騷的操作都已成為降級方案。
postMessage參考文檔
安全問題上述的各類非標準的騷操作,都算是對同源策略的破解辦法,在方便開發者完成跨域目的的同時,各類惡意的攻擊者也自然會利用這些方案為非作歹。
其中子域名代理的風險最低,因為需要服務器設置特定的子域名,也就是已經是兩個源的協商結果,一般黑客是難以模擬的。
風險最高的要算JSON-P的方案,因為這是任何客戶端都可隨意使用的辦法,CSRF攻擊的核心也是利用了特定標簽的跨域性發起請求,所以JSON-P最好用在無用戶狀態的低安全性API上。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/52138.html
摘要:同源策略年,同源政策由公司引入瀏覽器。標簽不受同源策略限制,但只能發起請求。這一行為使得不同域的特定文檔可以讀取該屬性值,因此可以繞過同源策略并使跨域消息通信成為可能。 前言 現在cross-origin resource sharing(跨域資源共享,下簡稱CORS)已經十分普及,算上IE8的不標準兼容(XDomainRequest),各大瀏覽器基本都已支持,當年為了前后端分離、if...
摘要:下面也是以模塊的模塊集為例,可以發現和路由有一些不同就是這里為了防止模塊跟全局耦合,運用函數式編程思想類似于依賴注入,將全局的實例作為函數參數傳入,再返回出一個包含的對象,這個導出的對象將會被以模塊名命名,合并到全局的集中。 前言 web前端發展到現代,已經不再是嚴格意義上的后端MVC的V層,它越來越向類似客戶端開發的方向發展,已獨立擁有了自己的MVVM設計模型。前后端的分離也使前端人...
摘要:下面也是以模塊的模塊集為例,可以發現和路由有一些不同就是這里為了防止模塊跟全局耦合,運用函數式編程思想類似于依賴注入,將全局的實例作為函數參數傳入,再返回出一個包含的對象,這個導出的對象將會被以模塊名命名,合并到全局的集中。 前言 web前端發展到現代,已經不再是嚴格意義上的后端MVC的V層,它越來越向類似客戶端開發的方向發展,已獨立擁有了自己的MVVM設計模型。前后端的分離也使前端人...
摘要:接上篇議題合理的架構討論上傳送門。處理思路如下使用上面定義的方法獲取如果能獲取到則說明有有效的,則時候即可跳轉到目標頁如果獲取到空字符串,則說明無效或不存在,跳轉至登錄頁面。 接上篇《【Geek議題】合理的VueSPA架構討論(上)》傳送門。 自動化維護登錄狀態 登錄狀態標識符跟token類似,都是需要自動維護有效期,但也有些許不同,獲取過程只在用戶登錄或注冊的時候,不需要自動獲取。 ...
摘要:接上篇議題合理的架構討論上傳送門。處理思路如下使用上面定義的方法獲取如果能獲取到則說明有有效的,則時候即可跳轉到目標頁如果獲取到空字符串,則說明無效或不存在,跳轉至登錄頁面。 接上篇《【Geek議題】合理的VueSPA架構討論(上)》傳送門。 自動化維護登錄狀態 登錄狀態標識符跟token類似,都是需要自動維護有效期,但也有些許不同,獲取過程只在用戶登錄或注冊的時候,不需要自動獲取。 ...
閱讀 1357·2021-10-09 09:44
閱讀 1440·2021-09-28 09:36
閱讀 15927·2021-09-22 15:55
閱讀 1239·2021-09-22 15:45
閱讀 2199·2021-09-02 09:48
閱讀 2783·2019-08-29 17:19
閱讀 2296·2019-08-29 10:54
閱讀 906·2019-08-23 18:40