跨域問題的場景和解決方案多種多樣,只要是作前端開發,總會遇到。並且面試時也是必問的問題。因此本身學習總結記錄一下。javascript
由於瀏覽器的同源策略,前端開發會遇到各類跨域問題。本篇文章總結了遇到跨域問題的不一樣的場景以及對應的解決方案。html
在總結各類跨域問題以前,咱們先來了解一下瀏覽器的同源策略。
協議、域名、端口都相同才叫同源。具體的這裏就不贅述了。前端
同源政策的目的,是爲了保證用戶信息的安全,防止惡意的網站竊取數據。
設想這樣一種狀況:A網站是一家銀行,用戶登陸之後,又去瀏覽其餘網站。若是其餘網站能夠讀取A網站的 Cookie,會發生什麼?
很顯然,若是 Cookie 包含隱私(好比存款總額),這些信息就會泄漏。更可怕的是,Cookie 每每用來保存用戶的登陸狀態,若是用戶沒有退出登陸,其餘網站就能夠冒充用戶,隨心所欲。由於瀏覽器同時還規定,提交表單不受同源政策的限制。
因而可知,"同源政策"是必需的,不然 Cookie 能夠共享,互聯網就毫無安全可言了。java
受到同源限制:nginx
1)沒法讀取不一樣源的 Cookie、LocalStorage 和 IndexDB 。
2)沒法得到不一樣源的DOM 。
3)不能向不一樣源的服務器發送ajax請求。web
不受同源限制:
在瀏覽器中,<script>
、<img>
、<iframe>
、<link>
等標籤均可以跨域加載資源,而不受同源策略的限制。面試
瀏覽器對跨域訪問的斷定:
CORS機制把跨域請求分爲兩類:簡單請求和非簡單請求。ajax
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判斷是否許可。若是許可就會帶上CORS相關想要頭,若是不在許可範圍內就不會帶上CORS相關的響應頭。瀏覽器再根據響應頭中是否有相關的CORS響應頭,來判斷攔截響應body和拋出錯誤。
非簡單請求:非簡單請求會在發真正的請求以前發送一個OPTIONS的帶着Origin、Access-Control-Request-Method、Access-Control-Request-Headers等CORS相關的請求頭的預檢請求到服務器,服務器確承認以這樣請求,就會返回帶着Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers等CORS相關的響應頭的響應,瀏覽器檢查到相關的CORS響應頭,說明經過預檢能夠繼續發送真正的請求;服務器確認不能夠,則不會返回這些相關響應頭,瀏覽器沒檢查到CORS的響應頭就會拋出錯誤。json
場景1:你的項目myweb,myweb的前端有一個接口是去訪問一個非myweb的服務器。非myweb服務器是第三方服務器,你不能去對第三方服務器作改動。
場景2:你的項目是個微服務架構的。那你的前端頁面可能就須要去不少個服務器上訪問數據。
原理解析:
跨域請求報錯歸根結底是瀏覽器禁止使用XHR對象向不一樣源的服務器地址發起HTTP請求。若是是服務器跨域向多個不一樣的服務器發送請求就不會有跨域問題存在。所以,咱們可讓瀏覽器只向一個服務器方式請求,讓這個服務器代替瀏覽器去不一樣的服務器上請求資源再返回給瀏覽器。這個服務器就是代理服務器了。
下面推薦一個經常使用代理服務器nginx。
什麼是nginx?
Nginx (engine x) 是一款輕量級的Web 服務器 、反向代理服務器及電子郵件(IMAP/POP3)代理服務器。
把ui所在的服務器和跨域服務器都用nginx代理轉發,瀏覽器訪問nginx,nginx到ui服務獲取ui,再把ui下載到瀏覽器,瀏覽器發起ui中的URL,該URL爲Nginx封裝後的跨域服務器的URL或ui服務器的URL,該URL到達Nginx以後,會被轉發到跨域服務器或ui服務器,請求處理完畢後,會經過Nginx中轉返回給瀏覽器。暴露出來的或者瀏覽器所發起的url都是nginx的url,nginx去跨域服務器和ui服務器獲取響應,返給瀏覽器,這樣就沒有跨域問題了。
場景:
先後端分離的開發模式下,在本地進行接口聯調時:也許在你的項目裏,你想嘗試先後端分離的開發模式。你在本地開發時,mock了一些假數據來幫助本身本地開發。而有一天,你但願在本地和後端同窗進行聯調。此時,後端rd的接口地址和你發生了跨域問題。這阻止了大家的聯調,你只能繼續使用你mock的假數據。
解決方案:
CORS須要瀏覽器和服務器同時支持。如何支持?請看瀏覽器對跨域訪問的斷定小節。
整個CORS通訊過程,都是瀏覽器自動完成,不須要用戶參與。對於開發者來講,CORS通訊與同源的AJAX通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感受。
所以,實現CORS通訊的關鍵是服務器。只要服務器實現了CORS接口,就能夠跨源通訊。
服務器要給接口的響應頭設置:Access-Control-Allow-Origin:*
場景:跨域發送get請求
jsonp解決跨域問題的本質: <script>
標籤能夠請求不一樣域名下的資源,即 <script>
請求不受瀏覽器同源策略影響。
首先給body動態添加一個 <script>
var script = document.createElement('script'); script.setAttribute("type","text/javascript"); script.src = 'http://example.com/ip?callback=foo'; document.body.appendChild(script); function foo(data) { console.log('Your public IP address is: ' + data.ip); };
上面的script會向 http://example.com/
服務器發送請求,這個請求的url後面帶了個callback參數,是用來告訴服務器回調方法的方法名的。由於服務器收到請求後,會把相應數據寫進foo的參數位置,也就是說服務器會返回的腳本以下
foo({ "ip": "8.8.8.8" });
這樣瀏覽器經過<script>
下載的資源就是上面的腳本了,<script>
下載完成就會當即執行,也就是說http://example.com/ip?callback=foo
這個請求返回後就會當即執行上面的腳本代碼,而這個腳本代碼就是調用回調方法和拿到json數據了。
場景1:你的http://www.damonare.cn/a.html
頁面裏使用<iframe>
調用另外一個http://damonare.cn/b.html
頁面。這時候你想在a頁面裏獲取b頁面裏的dom,而後進行操做。而後你會發現你不能得到b的dom。document.getElementById("myIFrame").contentWindow.document
或window.parent.document.body
由於兩個窗口不一樣源而報錯。
解決方案:這時候你只須要在a頁面裏和b頁面裏把document.domain
設置成相同的值就能夠在兩個頁面裏操做Dom了。
場景2:你在http://www.damonare.cn/a.html
頁面裏寫入了document.cookie = "test1=hello";
你在http://damonare.cn/b.html
頁面是拿不到這個cookie的。
解決方案:Cookie 是服務器寫入瀏覽器的一小段信息,只有同源的網頁才能共享。可是,兩個網頁一級域名相同,只是二級域名不一樣,瀏覽器容許經過設置document.domain共享 Cookie。另外,服務器也能夠在設置Cookie的時候,指定Cookie的所屬域名爲一級域名。這樣的話,二級域名和三級域名不用作任何設置,均可以讀取這個Cookie。
注意:document.domain
限制:雖然可讀寫,但只能設置成自身或者是高一級的父域且主域必須相同。因此只能解決一級域名相同二級域名不一樣的跨域問題。document.domain
只適用於 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 沒法經過這種方法跨域。
場景1:如今瀏覽器的一個標籤頁裏打開http://www.damonare.cn/a.html
頁面,你經過location.href=http://baidu.com/b.html
,在同一個瀏覽器標籤頁裏打開了不一樣域名下的頁面。這時候這兩個頁面你可使用window.name
來傳遞參數。由於window.name
指的是瀏覽器窗口的名字,只要瀏覽器窗口相同,那麼不管在哪一個網頁裏訪問值都是同樣的。
場景2:你的http://www.damonare.cn/a.html
頁面裏使用<iframe>
調用另外一個http://baidu.com/b.html
頁面。這時候你想在a頁面裏獲取b頁面裏的dom,而後進行操做。而後你會發現你不能得到b的dom。一樣會由於不一樣源而報錯,和上面提到的不一樣之處就是兩個頁面的一級域名也不相同。這時候document.domain
就解決不了了。
解決方案:瀏覽器窗口有window.name
屬性。這個屬性的最大特色是,不管是否同源,只要在同一個窗口裏,前一個網頁設置了這個屬性,後一個網頁能夠讀取它。。好比你在b頁面裏設定window.name="hello"
,你再返回到a頁面,在a頁面裏訪問window.name
,能夠獲得hello
。
這種方法的優勢是,window.name容量很大,能夠放置很是長的字符串;缺點是必須監聽子窗口window.name屬性的變化,影響網頁性能。
場景1:在a頁面裏打開了另外一個不一樣源的頁面b,你想要讓a和b兩個頁面互相通訊。好比,a要訪問b的LocalStorage。
場景2:你的a頁面裏的iframe
的src是不一樣源的b頁面,你想要讓a和b兩個頁面互相通訊。好比,a要訪問b的LocalStorage。
解決方案:HTML5y引入了一個全新的API,跨文檔通訊 API(Cross-document messaging)。這個API爲window對象新增了一個window.postMessage
方法,容許跨窗口通訊,不論這兩個窗口是否同源。a就能夠把它的LocalStorage,發送給b,b也能夠把本身的LocalStorage發給a。
window.postMessage(message, targetOrigin, [transfer]),有三個參數:
message是向目標窗口發送的數據;
targetOrigin屬性來指定哪些窗口能接收到消息事件,其值能夠是字符串"*"(表示無限制)或者一個URI(或者說是發送消息的目標域名);
transfer可選參數,是一串和message 同時傳遞的 Transferable 對象. 這些對象的全部權將被轉移給消息的接收方,而發送一方將再也不保有全部權。
另外消息的接收方必須有監聽事件,不然發送消息時就會報錯。The target origin provided ('http://localhost:8080') does not match the recipient window's origin ('http://localhost:63343').
window.addEventListener("message",onmessage);
onmessage接收到的message事件包含三個屬性:
data:從其餘 window 中傳遞過來的數據。
origin:調用 postMessage 時消息發送方窗口的 origin 。請注意,這個origin不能保證是該窗口的當前或將來origin,由於postMessage被調用後可能被導航到不一樣的位置。
source:對發送消息的窗口對象的引用; 您可使用此來在具備不一樣origin的兩個窗口之間創建雙向通訊。
例子:我在a頁面執行
var popup = window.open('http://localhost:3000', 'title'); popup.postMessage('Hello World!', 'http://localhost:3000');
同時在http://localhost:3000
的頁面裏監聽message事件:
window.onload=function () { window.addEventListener("message",onmessage); } function onmessage(event) { if(event.origin=="http://localhost:63343"){//http://localhost:63343是發送方a的域名 console.log(event.data);//'Hello World!' } console.log(event.data);//'Hello World!' }
注意: 在 Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)以前, 參數 message 必須是一個字符串。 從 Gecko 6.0 (Firefox 6.0 / Thunderbird 6.0 / SeaMonkey 2.3)開始,參數 message被使用結構化克隆算法進行序列化。這意味着您能夠將各類各樣的數據對象安全地傳遞到目標窗口,而沒必要本身序列化它們。
location.hash就是指URL的#號後面的部分。
場景:
父窗口和iframe的子窗口之間通信或者是window.open打開的子窗口之間的通信。
解決方案:
父窗口改變子窗口的url的#號後面的部分,後者把要傳遞的參數寫在#後面,子窗口監聽window.onhashchange事件,獲得通知,讀取window.location.hash解析出有用的數據。一樣子窗口也能夠向父窗口傳遞數據。