原文連接:http://www.devsai.com/2016/11/24/talk-CORS/javascript
1995年,同源政策由 Netscape 公司引入瀏覽器。爲了防止某些文檔或腳本加載別的域下的未知內容,防止形成泄露隱私,破壞系統等行爲發生。html
同源策略作了兩種限制:前端
不能經過ajax的方法或其餘腳本中的請求去訪問不一樣源中的文檔。html5
瀏覽器中不一樣域的框架之間是不能進行js的交互操做的。java
如今全部的可支持javascript的瀏覽器都會使用這個策略。web
URL的三部分徹底相同時咱們就能夠稱其爲同源,這三部分是: 協議
,域名(主機名)
和端口
都相同。ajax
當涉及到同源策略時,Internet Explorer有兩個主要的例外json
授信範圍(Trust Zones):兩個相互之間高度互信的域名,如公司域名(corporate domains),不遵照同源策略的限制。 端口:IE未將端口號加入到同源策略的組成部分之中,所以 http://company.com:81/index.html 和http://company.com/index.html 屬於同源而且不受任何限制。
雖然同源策略頗有必要,但有不少時候咱們仍是須要去請求其餘域的數據,如:調用不一樣業務的數據,而不一樣業務已子域區分;又或者是第三方公用的數據接口等等canvas
因爲各類緣由,咱們須要經過各類方式來請求到不一樣域下的資源。windows
jsonp是經過能夠發出跨域請求的script標籤,使javascript可以得到跨域請求的數據,並調用數據。
先看個例子:
文件index.js :
alert(123);
頁面index.html:
... <script src="./index.js"></script> ...
當加載頁面index.html
後,出123
內容的彈窗。經過查看index.js
的響應體,會發現響應內容就是alert(123)
。
因此,能夠這麼思考,只要是經過script標籤請求到的內容就會被當作js代碼執行。
是否能夠在script中的地址src
不請求js文件,而是請求服務端的接口(即便不在同源下的),那麼返回的內容就能得到到,而且會當成js代碼來執行。(通常的script標籤都會去請求js代碼文件)
再來看下正常的服務端獲取數據接口。
好比:有這麼個接口
/getUserInfo/001
,經過ajax請求得到此接口數據{"data" : {"name" : "devsai",like:"everything"}}
。獲得數據後在ajax中調用
showUserInfo(data)
來渲染頁面,data
就是接口數據。
若是如今用script標籤來請求數據,那麼一樣能夠得到數據,執行返回到的內容,因是json
格式的數據,並不會報錯,但也並無卵用。得到接口的數據確定是想作些什麼的。
再想一想,正常ajax請求後的js執行內容showUserInfo(data)
,拿到數據後,調用了showUserInfo
函數。
那麼,用script標籤來請求數據時,返回的內容直接是showUserInfo(data)
不就好了,但服務端又不知道咱們到底要執行哪一個函數,即便事先約定了,但後面因某些事要改,那還得告訴服務端,太麻煩了。
若是知道要執行什麼函數就行了。
固然,這是能夠的,改造下接口,以參數的形式把函數名傳給服務端。
<script src="/jsonp/getUserInfo/001?jsonp_fn=showUserInfo"></script>
Response返回的內容一樣須要改造
Response: showUserInfo({"data" : {"name" : "devsai",like:"everything"}})
這樣,經過jsonp,去跨域請求接口數據就完成了。
須要注意的是函數名須要掛在window
下面,要否則會報函數名未定義。
例如在demo.devsai.com/index.html
頁面裏執行以下內容:
document.domain = 'devsai.com';
執行該語句後,能夠成功經過devsai.com/index.html
的同源檢測, 實現數據的通信,
固然document.domain
不能隨意設置,只能設置成當前域,或設置成當前域的頂域。
document.domain經常被用於同站但不一樣域的狀況,例如:www.devsai.com
,下嵌入了iframe廣告頁面ad.devsai.com
,想要實現兩頁面的通信,就須要對兩個頁面都設置document.domain='devsai.com'
。
window對象有個name屬性,該屬性有個特徵:即在一個窗口(window)的生命週期內,窗口載入的全部的頁面都是共享一個window.name的,每一個頁面對window.name都有讀寫的權限,window.name是持久存在一個窗口載入過的全部頁面中的,並不會因新頁面的載入而進行重置。
name只能是字符串。
頁面a.html中:
<script> window.name = 'page name index.html'; setTimeout(function(){ window.location = 'http://localhost:8080/static/b.html'; }, 2000); </script>
頁面b.html:
<script> alert(window.name); </script>
再來看看如何讓a.html頁面獲取數據
用data.html做爲請求數據地址:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Data</title> </head> <body> <script> window.name = '{ "name":"devsai","like" : "everything"}'; // 須要傳入 a.html頁面的數據,必須是字符串 </script> </body> </html>
a.html:
... <iframe src="http://localhost:8080/static/data.html" onload="getData();" frameborder="0" id="iframe_1"></iframe> <script> function getData(){ var iframe = document.getElementById('iframe_1'); //隱藏iframe iframe.setAttribute("width", "0"); iframe.setAttribute("height", "0"); iframe.setAttribute("border", "0"); iframe.setAttribute("style", "width: 0; height: 0; border: none;"); iframe.onload = function(){ console.log(iframe.contentWindow.name); var data = iframe.contentWindow.name; data = JSON.parse(data);//轉成 JSON showUserInfo(data); } iframe.src = 'about:blank'; } function showUserInfo(data){ console.log(data); // .....do something } </script> ...
當訪問http://127.0.0.1:8080/static/index.html
,便能得到來自不一樣域下data.html
中的數據。
也能夠作的更完善些,動態的生成iframe請求數據,用完即毀。
.... // 傳入請求數據接口地址和回調函數 function requestData(url,successCB){ var body = document.getElementsByTagName('body')[0]; var iframe = document.createElement("iframe"); iframe.setAttribute("id", "getDataByWindowName"); iframe.setAttribute("width", "0"); iframe.setAttribute("height", "0"); iframe.setAttribute("border", "0"); iframe.setAttribute("style", "width: 0; height: 0; border: none;"); iframe.setAttribute("src", url); body.appendChild(iframe); setTimeout(function(){//防止iframe.src在沒加載前就被替換 iframe.onload = function(){ var data = iframe.contentWindow.name; if(data){ data = JSON.parse(data);//轉成 JSON successCB && successCB(data); } iframe.parentNode.removeChild(iframe); } iframe.src = 'about:blank'; }, 100); } //requestData("http://localhost:8080/static/data.html",showUserInfo); ...
這就是使用window.name來進行跨域。
window.postMessage方法是html5的新特性之一,
可使用它來向其它的window對象發送消息,無論這個window對象是屬於同源或不一樣源。
經過window.postMessage容許瀏覽器windows, tabs, and iFrames之間跨域通信。
以前寫過一篇關於window.postMessage
的,作了詳細的說明+演示頁面+演示代碼,去看看
例如一個網站上有各類不一樣的業務,不一樣的業務有其對應的子域。
如:ad.devsai.com;upload.devsai.com;live.devsai.com
,分別對應廣告業務,上傳業務,直播業務。
想在www.devsai.com
中作交互,或得到數據,便會受跨域影響。
形成跨域的緣由是由於請求數據的源不一樣,那隻要請求的源同樣,便沒有跨域問題了。
這也是能夠辦到的,只須要web服務作下代理,或稱之爲地址映射。
拿Nginx舉例,須要在web服務上作以下配置:
... lcaotion /ad { proxy_pass http://ad.devsai.com } location /upload { proxy_pass http://upload.devsai.com } location /live { proxy_pass http://live.devsai.com } ...
而後就能夠在以www.devsai.com/ad/
的方式去調用廣告業務。
當一個發起的請求地址與發起該請求自己所在的地址不在同源下時,稱該請求發起了一個跨域的HTTP請求。
有些的跨域請求是被容許的<img>
,<script>
,<link>
圖片,腳本,樣式及其餘資源 ,加載這些數據時即便不在同源下面也一樣被容許,現在的網站一般也會去引用不在同源下的這些資源,如作CDN加速。
但也有些不被容許,正如你們所知,出於安全考慮,瀏覽器會「限制」腳本中發起的跨站請求,好比:XmlHttpRequest。
除了XmlHttpRequest外,還有如下幾種跨域請求作了相應的安全限制。
好比:
1 前面說的iframe,經過設置src
能夠發起跨域請求,但對請求到的內容進行操做就不被容許了。如執行iframe.contentWindow.name
就會報錯。
2 <img>
標籤上crossorigin
屬性是一個CORS的配置屬性,目的是爲了容許第三方網站上的圖片(即不在同源上的圖片)可以在canvas中被使用。
若是沒有配置改屬性,又跨域請求了圖片,當調用canvas中的toBlob()
, toDataURL()
, 或getImageData()
方法的時候,會報錯。
var img = new Image, canvas = document.createElement("canvas"), ctx = canvas.getContext("2d"), src = "https://sf-sponsor.b0.upaiyun.com/45751d8fcd71e4a16c218e0daa265704.png"; // insert image url here img.crossOrigin = "Anonymous"; img.onload = function() { console.log(img); canvas.width = img.width; canvas.height = img.height; ctx.drawImage( img, 0, 0 ); localStorage.setItem( "savedImageData", canvas.toDataURL("image/png") ); } img.src = src;
3 一樣的script也能夠有crossorigin
屬性,
script自己沒有跨域問題,否則jsonp就無法用了。但若是請求的不是同源下的js文件,發生錯誤後,沒法經過window.onerror事件捕捉到詳細的信息
例如加載index.js文件,其中a未定義:
var b = a;
同源下的 window.onerror報錯信息
跨域下的 window.onerror報錯信息
經過script標籤上添加crossdomain屬性,並在服務上配置響應頭。
<script src="http://lcoalhost:/static/index.js" type="text/javascript" charset="utf-8" crossdomain></script>
在去看onerror中的報錯信息就和同源下的報錯信息同樣了。
4 Web字體 (CSS 中經過 @font-face 使用跨站字體資源),使用非同源地址,一樣會報錯。
還須要注意的一點是,跨域請求並不是是瀏覽器限制了請求,而是瀏覽器攔截了返回結果。不論是否跨域,請求都會發送到服務端。
但也有特例,有些瀏覽器不容許從HTTPS的域跨域訪問HTTP,好比Chrome和Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求。
解決這類跨域問題的方法就是*CORS*
,對於簡單的請求來講,前端這邊都不須要作任何的編碼就能實現跨域請求,
只須要服務端配置響應頭"Access-Control-Allow-Origin:*"。
CORS是一個W3C標準,全稱「跨域資源共享」(Cross-origin resource sharing)
跨源資源共享標準經過新增一系列 HTTP 頭,讓服務器能聲明哪些來源能夠經過瀏覽器訪問該服務器上的資源。
Access-Control-Allow-Origin
根據Reuqest請求頭中的Origin來判斷該請求的資源是否能夠被共享。
若是Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin字段(該字段的值爲服務端設置Access-Control-Allow-Origin的值)便知出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。此時HTTP的返回碼爲200,因此 這種錯誤沒法經過狀態碼識別。
Access-Control-Allow-Credentials
指定是否容許請求帶上cookies,HTTP authentication,client-side SSL certificates等消息。
如須要帶上這些信息,Access-Control-Allow-Credentials:true
並須要在XmlHpptRequest中設置xhr.withCredentials=true
。
需注意的是,當設置了the credentials flag爲true,那麼Access-Control-Allow-Origin就不能使用"*
"
Access-Control-Max-Age
可選字段,指定了一個預請求將緩存多久,在緩存失效前將不會再發送預請求。
Access-Control-Allow-Methods
做爲預請求Response的一部分,指定了真實請求可使用的請求方式。
Access-Control-Allow-Headers
做爲預請求Response的一部分,指定了真實請求可使用的請求頭名稱(header field names)。
CORS的有兩種請求方式: 簡單請求(Simple Request) 和 預請求(Prefilght Request)
只要同時知足如下三大條件,就屬於簡單請求。
a) 請求方式是如下幾種方式之一
* GET * POST * HEAD
b) content-type必須是如下幾種之一
* application/x-www-form-urlencoded * multipart/form-data * text/plain
c) 不會使用自定義請求頭(相似於 X-Modified 這種)。
若是不知足簡單請求的三大條件,會在發送正真的請求前,發送個請求方式爲'OPTIONS'的請求,去服務端作檢測,
1) 請求方式不是GET,POST,HEAD
那麼須要在響應HEAD配置容許的請求方式,例如:Access-Control-Allow-Methods:PUT,DELETE
2) 使用自定義請求頭,如x-devsai ,那麼須要在服務端相應配置容許的自定義請求頭:Access-Control-Request-Headers: x-devsai
一旦檢測不經過,瀏覽器就會提示相應的報錯,並不會發生真實的請求。
從上圖可只IE11,如下的就不支持CORS了。但實際上再IE8,IE9,IE10中,能夠用XDomainRequest對象代替XmlHttpReuqest,發送跨域請求。
var xdr = new XDomainRequest(); xdr.open("get", "http://www.devsai.com/xdr"); xdr.send();
最後,總結下各類跨域方案的特色,還記得本文開始說的,同源策略的兩種限制嗎?
不能經過__ajax的方法__或__其餘腳本中的請求__去訪問不一樣源中的文檔。
瀏覽器中不一樣域的框架之間是不能進行js的交互操做的。
把第1種標記爲__TYPE_1__,第二種標記爲__TYPE_2__,對上述的幾種解決跨域的方法分下類。
window.name 須要注意name只能是字符串
解決的限制 :__TYPE_1__,__TYPE_2__
缺點: 接口返回的內容必須都是html裏嵌入script腳本。
document.domain 經過修改domain跨子域
解決的限制 :__TYPE_2__
缺點: 僅支持同個域下的子域跨域,跨域能力有限
window.postMessage 用於iframe、window、tabs之間的跨域通信
解決的限制 :__TYPE_2__
缺點: 兼容問題,IE10如下受限,IE8如下無效
jsonp 是以前最經常使用的解決跨域請求的方法。
解決的限制 :__TYPE_1__
缺點: 不能用於POST
請求
服務端地址映射 前端不須要管,並能解決跨域請求問題的一種方法。
解決的限制 :__TYPE_1__
缺點: 非要說缺點,那就是要說服服務端同窗,並且通常場子鋪大了的公司只用同源,不太可能。
CORS 感受目前比較經常使用的解決跨域請求的方法。
解決的限制 :__TYPE_1__
缺點: 也是兼容性問題
真正開發過程當中,需針對不一樣狀況,使用不一樣的解決之法。