「同源策略」(Same Origin Policy)是瀏覽器安全的基礎。javascript
同源策略限制從一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的關鍵的安全機制。
在判斷兩個頁面的url是否具備相同的源以前,咱們先來看一下一個url(統一資源定位符)的基本組成部分。html
對於一個url,它的基本組成部分是:協議 :// 域名(或者是ip) : 端口(若是指定了) / 路徑。
那麼下面咱們來舉個例子:前端
對於http://www.example.com/static
來講,協議就是http
,它的域名是www.example.com
,它的路徑是static
,這裏的端口號省略了(默認爲80)。java
值得一提的是,在這個域名下面,example.com
是主域名,而www.example.com
是子域名,一樣的a.example.com
也是一個與前面所述不一樣的另外一個子域名。而且,上面這三個域名互不相同。node
這裏再也不展開贅述,相關知識請查閱與計算機網絡有關的知識。jquery
那如何判斷頁面是否具備相同的源呢?git
若是協議,端口(若是指定了)和域名對於兩個頁面是相同的,則兩個頁面具備相同的源。
也就是說,判斷同源的三要素是:協議、端口、域名。github
須要注意的是,若是是一個域名二級域名,好比上面提到的www.example.com
,它與另一個二級域名a.example.com
,雖然他們的主域相同,可是子域不一樣,因而這兩個就不是同一個域名,因此也不能說是同源。三級域名依次類推...web
下表給出了相對http://store.company.com/dir/page.html
同源檢測的示例:ajax
說到同源策略,必不可少的就是Cookie這個東西了。
而講到Cookie,跟它關聯在一塊兒的又有Session。
對於這二者,這裏不作大篇幅的介紹,具體去傳送門查閱。
這裏咱們作一下簡要的總結:
Set-Cookie
能夠指定生成Cookie的內容和生命週期,若是由瀏覽器生成則在關掉瀏覽器後失效。最後,對Cookie和Session實現的身份認證和狀態保持功能作一個舉例。
假設如今有一個學生信息管理系統,此時數據庫已經有學生的相關信息。(帳號、密碼、我的信息等等)
而後當學生登陸這個系統,經過POST請求把用戶的帳戶密碼發送到後臺服務器。當後臺服務器接收到這些參數的時候,會跟數據庫保存的記錄進行匹配。
一旦匹配成功,也就是用戶的帳號密碼都正確的狀況下,這個時候後臺服務器會在Session中記錄一個值,能夠是用戶名或者其餘可以惟一標識用戶的字段。
當把這個值保存在Session中後,後臺服務器會返回響應告知客戶端登陸成功,能夠進行後續的操做。此時,後臺服務器會在HTTP響應報文中添加一個字段Set-Cookie
,它的值是當前Session的SessionID,(這個SessionID是指向咱們當前的那個Session的,在Node的Express中express-session會封裝好這個過程)固然還會設置Cookie的其餘屬性,好比說過時時間Expires
等等。
當瀏覽器接收到這個HTTP響應報文的時候,就會在本地設置一個Cookie,它的過時時間由響應報文中Set-Cookie
中的Expires
字段的值決定,若是爲空,則關閉瀏覽器(即會話結束時)後失效。
以後,每次向後臺服務器發送請求的時候,瀏覽器默認會把這個Cookie加在HTTP請求報文的Cookie中。這樣,每次後臺服務器接收到請求的時候,會根據Cookie中的SessionID去找到咱們的Session。
假如這個SessionID映射獲得Session,那麼這個時候說明瀏覽器是已經登陸過了。因而,就能夠進行後續的一些相關的操做。
另外,值得一提的是,Session機制決定了當前客戶只會獲取到本身的Session,而不會獲取到別人的Session。各客戶的Session也彼此獨立,互不可見。也就是說,當多個客戶端執行程序時,服務器會保存多個客戶端的Session。獲取Session的時候也不須要聲明獲取誰的Session。
這就是Cookie和Session作狀態保持和身份驗證的一個具體的例子。
說到同源限制,還有一個不得不提的就是iframe。
iframe能夠在父頁面中嵌入一個子頁面,在平常開發中一旦使用,避免不了的就要涉及到不一樣的iframe頁面進行通訊的問題,多是得到其餘iframe的DOM,或者是獲取其餘iframe上的全局變量或方法等等。
同源下的iframe,也就是iframe中的src
屬性的URL符合同源的條件,那麼經過iframe的contentDocument
和contentWindow
獲取其餘iframe的DOM或者全局變量、方法都是很簡單的事情。
那若是是非同源的兩個iframe,單純的經過變量訪問的方式就受到同源限制了。
爲了解決這個問題,HTML5引入了一個新的API:postMessage
,主要就是用來解決存在跨域問題的iframe頁面之間通訊的問題。
下面簡單的舉一個例子,假如如今有兩個不一樣的頁面,A頁面的url是http://localhost:4002/parent.html
,B頁面的url的是http://localhost:4003/child.html
,如今我把B頁面用iframe嵌在A頁面下面,代碼(精簡)是這樣子的。如今我要實現的是向子頁面B傳遞一個消息:
A頁面代碼:
<body> <h1>A頁面</h1> <iframe src="http://localhost:4003/child.html" id="child"> </iframe> <script> window.onload = function() { document.getElementById("child").contentWindow.postMessage("父頁面發來賀電", "http://localhost:4003"); } </script> </body>
B頁面代碼:
<body> <h1>B頁面</h1> <script> window.onload = function() { window.addEventListener("message", function(e) { //判斷信息的來源是否來自於父頁面,保證信息源的安全 if(e.source != window.parent) return; alert(e.data); }); }; </script> </body>
結果如圖:
postMessage
接受兩個參數,一個是要傳送的data
,另一個是目標窗口的源,若是想傳給任何窗口,能夠設置成*
。
目標頁面接收信息的時候,使用的是window.addEventListener("message", function() {})
。
固然也有不受同源限制的狀況存在,主要有如下列舉的:
script
標籤容許跨域嵌入腳本,稍後介紹的JSONP就是利用這個「漏洞」來實現。img
標籤、link
標籤、@font-face
不受跨域影響。video
和audio
嵌入的資源。iframe
載入的任何資源。(不是iframe之間的通訊)<object>
、<embed>
和<applet>
的插件。WebSocket
不受同源策略的限制。注:如下跨域資源共享的兩種請求辨析內容摘抄至阮一峯的《跨域資源共享》一文。
CORS是一個W3C標準,全稱是「跨域資源共享」(Cross-origin resource sharing)。
它容許瀏覽器向跨源服務器發出XMLHttpRequest請求,從而克服了AJAX只能同源發送請求的限制。
實現CORS主要在於服務器的設置,關鍵在於服務器HTTP響應報文首部的設置。前端部分大體仍是跟原來發AJAX請求沒什麼區別,只是須要對AJAX進行一些相關的設置,稍後咱們都會講到。
在講解如何實現跨域資源共享的時候,咱們先來看一下CORS的兩種請求。
瀏覽器將CORS分爲兩種請求,一種是簡單請求,另一種對應的確定就是非簡單請求。
只要同時知足下面兩大條件,就屬於簡單請求:
請求的方法是一下的三種方法之一:
HTTP的頭信息不超過如下幾種字段:
application/x-www-form-urlencoded
、multipart/formdata
、text/plain
。凡是不一樣時知足以上兩種條件,就屬於非簡單請求。
瀏覽器對於兩種請求處理是不同的。
對於簡單請求,瀏覽器直接發出CORS請求。具體來講,就是在HTTP請求報文首部,增長一個Origin
字段。以下:
GET /cors HTTP/1.1 Origin: http://api.bob.com Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
上面Origin
字段的用來講明本次請求來自哪一個源(協議+域名+端口)。服務器根據這個值,決定是否贊成此次請求。
若是Origin
指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin
字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror
回調函數捕獲。注意,這種錯誤沒法經過狀態碼識別,由於HTTP迴應的狀態碼有多是200。
若是Origin
指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段。
Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8
上面的HTTP響應報文首部信息中,有三個與CORS請求相關的字段,都是以Access-Control-
開頭。
Access-Control-Allow-Origin
該字段是必須的,它的值要麼是請求Origin
字段,要麼是一個*
,表示接受任意域名的請求。
Access-Control-Allow-Credentials
該字段可選。它的值是一個布爾值,表示是否容許發送Cookie。默認狀況下,Cookie不包括在CORS請求之中。設爲true
,即表示服務器明確許可,Cookie能夠包含在請求中,一塊兒發給服務器。這個值也只能設爲true
,若是服務器不要瀏覽器發送Cookie,刪除該字段便可。
值得一提的是,若是想要CORS支持Cookie,不只要在服務器指定HTTP響應報文首部字段,還須要在AJAX中打開withCredentials
的屬性。(jQuery中AJAX設置後面會講到)
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
有些瀏覽器在省略withCredentials
設置的時候,仍是會發送Cookie。因而,能夠顯式關閉這個屬性。
xhr.withCredentials = false;
須要注意的是,若是要發送Cookie,Acess-Control-Allow-Origin
不能設置爲*
,必須設置成具體的域名,若是是本地調試的話能夠考慮設置成null
。
Access-Control-Expose-Headers
該字段可選。CORS請求時,XMLHttpRequest對象的getResponseHeader()
方法只能拿到6個基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。若是想拿到其餘字段,就必須在Access-Control-Expose-Headers
裏面指定。上面的例子指定,getResponseHeader('FooBar')
能夠返回FooBar
字段的值。
非簡單請求是那種對服務器有特殊要求的請求,好比請求方法是PUT或DELETE,或者Content-Type
字段的類型是application/json
。
非簡單請求的CORS請求,會在正式通訊以前,增長一次HTTP查詢請求,稱爲"預檢"請求(preflight)。
瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些HTTP動詞和頭信息字段。只有獲得確定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,不然就報錯。
下面是一段JavaScript腳本:
var url = 'http://api.alice.com/cors'; var xhr = new XMLHttpRequest(); xhr.open('PUT', url, true); xhr.setRequestHeader('X-Custom-Header', 'value'); xhr.send();
很明顯,這是一個非簡單請求,使用了PUT方法來發送請求,而且自定義了一個HTTP請求報文的首部字段。
因而,瀏覽器發現這是一個非簡單的請求,就自動發出了一個「預檢」請求,要求服務器確承認以這樣請求。下面是這個「預檢」請求的HTTP頭信息。
OPTIONS /cors HTTP/1.1 Origin: http://api.bob.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: X-Custom-Header Host: api.alice.com Accept-Language: en-US Connection: keep-alive User-Agent: Mozilla/5.0...
"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭信息裏面,關鍵字段是Origin
,表示請求來自哪一個源。
除了Origin
字段,「預檢」請求的頭信息還包括兩個特殊字段。
Access-Control-Request-Method
該字段是必須的,用來列出瀏覽器的CORS會用到哪些HTTP方法,上面是PUT。
Access-Control-Request-Headers
該字段是一個用逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息。上面的例子是X-Custom-Header
。
因而,服務器收到「預檢」請求以後,檢查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段之後,確認容許跨域請求,就能夠作出迴應。
HTTP/1.1 200 OK Date: Mon, 01 Dec 2008 01:15:39 GMT Server: Apache/2.0.61 (Unix) Access-Control-Allow-Origin: http://api.bob.com Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Content-Type: text/html; charset=utf-8 Content-Encoding: gzip Content-Length: 0 Keep-Alive: timeout=2, max=100 Connection: Keep-Alive Content-Type: text/plain
若是瀏覽器否認了"預檢"請求,會返回一個正常的HTTP迴應,可是沒有任何CORS相關的頭信息字段。這時,瀏覽器就會認定,服務器不一樣意預檢請求,所以觸發一個錯誤,被XMLHttpRequest對象的onerror
回調函數捕獲。控制檯會打印出以下的報錯信息。
XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服務器迴應的其餘CORS相關字段以下:
Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000
對比簡單請求服務器響應的CORS字段,發現多了三個:
Access-Control-Allow-Methods
該字段必需,它的值是逗號分隔的一個字符串,代表服務器支持的全部跨域請求的方法。注意,返回的是全部支持的方法,而不單是瀏覽器請求的那個方法。這是爲了不屢次"預檢"請求。
Access-Control-Allow-Headers
若是瀏覽器請求包括Access-Control-Request-Headers
字段,則Access-Control-Allow-Headers
字段是必需的。它也是一個逗號分隔的字符串,代表服務器支持的全部頭信息字段,不限於瀏覽器在"預檢"中請求的字段。
Access-Control-Max-Age
該字段可選,用來指定本次預檢請求的有效期,單位爲秒。上面結果中,有效期是20天(1728000秒),即容許緩存該條迴應1728000秒(即20天),在此期間,不用發出另外一條預檢請求。
因而,一旦瀏覽器經過了「預檢」,之後每次瀏覽器正常的CORS請求,都跟簡單請求同樣,會有一個Origin
頭信息字段。服務器的迴應,也都有一個Access-Control-Allow-Origin
頭信息字段。若是開啓了Cookie設置,那還有一個Access-Control-Allow-Credentials:true
。
那怎麼在Node中結合Express設置後臺的跨域部分呢?
其實很簡單,須要設置的就是上面所述的幾個響應首部的字段,主要考慮兩種類型的請求和是否須要使用Cookie。具體設置以下:
app.all("*", function(req, res, next) { res.header("Access-Control-Allow-Origin", /* url | * | null */); res.header("Access-Control-Allow-Headers", "Authorization, X-Requested-With"); res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS"); /* 服務器支持的全部字段 */ res.header("Access-Control-Allow-Credentials", "true"); /* 當使用Cookie時 */ res.header("Access-Control-Max-Age", 300000); /* 設置預檢請求的有效期 */ if (req.method === "OPTIONS") return res.send(200); /*讓options請求快速返回*/ else next(); });
上面的設置有幾個須要注意的地方:
file://...
之類的url),能夠把Access-Control-Allwo-Origin
的值設置爲Null,這樣子就可以使用Cookie。若是設置成*
,雖然也能夠跨域發送請求,可是這個時候沒有辦法使用Cookie。Access-Control-Allow-Headers
字段不是必須的,僅當發送的請求有Access-Control-Request-Headers
時須要設置。Access-Control-Allow-Methods
字段。若是使用jQuery封裝的AJAX發送請求,那麼須要在相應的JS代碼設置:
$.ajaxSetup({ xhrFields: { withCredentials: true }, crossDomain: true });
withCredentials
是設置CORS發送Cookie,默認是不發送的。 crossDomain
告知AJAX容許跨域。
以上就是CORS設置跨域的具體介紹。
對於JSONP來講,前面也已經提到了,其實它是利用了某些不受同源限制的標籤的所謂「漏洞」,來實現「曲線救國」式的跨域的方案。
它借用script
標籤不受同源限制的這個特性,經過動態的給頁面添加一個script
標籤,利用事先聲明好的數據處理函數來獲取數據。
值得一提的是,JSONP這種方法其實和CORS有很大的區別,它並不屬於一種規範。所謂的JSONP是應用JSON數據的一種新方法,它只不過是被包含在函數調用中的JSON。
在JSONP中包含兩部分:回調函數和數據。其中,回調函數是當響應到來時要放在當前頁面被調用的函數。而數據,就是傳入回調函數中的JSON字符串,也就是回調函數的參數了。下面咱們簡單模擬一下JSONP的通訊過程。
JSONP的原理詳細講解能夠看這個傳送門。
咱們來簡單的模擬一下JSONP的通訊過程。
function handleResponse(response) { console.log(response.data); } var script = document.createElement("script"); script.src = "http://example.com/jsonp/getSomething?uid=123&callback=hadleResponse" document.body.insertBefore(script, document.body.firstChild); /*handleResponse({"data": "hey"})*/
它的過程是這樣子的:
script
標籤請求時,後臺會根據相應的參數來生成相應的JSON數據。好比說上面這個連接,傳遞了handleResponse
給後臺,而後後臺根據這個參數再結合數據生成了handleResponse({"data": "hey"})
。另外,想要實現JSONP,後臺服務器也必須作相應的設置。
值得一提的是,JSONP是存在必定的侷限性的:
下面是一個實現JSONP的庫,咱們來一塊兒分析一下它的源代碼。這是源碼的github地址。
/** * Module dependencies */ var debug = require('debug')('jsonp'); //使用依賴 /** * Module exports. */ module.exports = jsonp; //輸出模塊 /** * Callback index. */ var count = 0; //回調函數的index值,便於取名。 /** * Noop function. */ function noop(){} //無操做空函數,以便使用後把window[id]置空 /** * JSONP handler * * Options: * - param {String} qs parameter (`callback`) * - prefix {String} qs parameter (`__jp`) * - name {String} qs parameter (`prefix` + incr) * - timeout {Number} how long after a timeout error is emitted (`60000`) * * @param {String} url * @param {Object|Function} optional options / callback //這裏的callback是取得數據後的callback,不是傳給服務器的callback * @param {Function} optional callback */ function jsonp(url, opts, fn){ if ('function' == typeof opts) { fn = opts; opts = {}; } if (!opts) opts = {}; var prefix = opts.prefix || '__jp'; // use the callback name that was passed if one was provided. // otherwise generate a unique name by incrementing our counter. var id = opts.name || (prefix + (count++)); var param = opts.param || 'callback'; var timeout = null != opts.timeout ? opts.timeout : 60000; var enc = encodeURIComponent; var target = document.getElementsByTagName('script')[0] || document.head; var script; var timer; //必定時間內後臺服務器沒有返回視爲超時 if (timeout) { timer = setTimeout(function(){ cleanup(); if (fn) fn(new Error('Timeout')); }, timeout); } //回覆原始設置、清空狀態 function cleanup(){ if (script.parentNode) script.parentNode.removeChild(script); window[id] = noop; if (timer) clearTimeout(timer); } //取消操做 function cancel(){ if (window[id]) { cleanup(); } } //聲明函數,等待script標籤加載的url引入完畢後調用 window[id] = function(data){ debug('jsonp got', data); cleanup(); if (fn) fn(null, data);//node中約定第一個參數爲err,可是這裏不傳,直接就置爲null }; // add qs component url += (~url.indexOf('?') ? '&' : '?') + param + '=' + enc(id); url = url.replace('?&', '?'); debug('jsonp req "%s"', url); // create script script = document.createElement('script'); script.src = url; target.parentNode.insertBefore(script, target); //引入script標籤後會直接去調用聲明的函數,而後函數會把script標籤帶有的data給傳出去 return cancel; //返回初始狀態 }
接着,咱們能夠利用上面的這個庫,給它進行一個封裝,下面是咱們本身寫的_jsonp
函數:
/* 這個是本身定義的一個_jsonp */ /** * @param {String} url * @param {Object} data * @param {Object} option * @returns */ function _jsonp(url, data, option) { url += (url.indexOf('?') < 0 ? '?' : '&') + param(data); return new Promise((resolve, reject) => { jsonp(url, option, (err, data) => { if (!err) { resolve(data); } else { reject(err); } }); }); /* 這裏把jsonp封裝成了一個promise對象,回調函數中若是成功的話會把數據帶回來而後resolve出去 */ } //緊接着是對參數的一個序列化 function param(data) { let url = ''; for (var k in data) { let value = data[k] !== undefined ? data[k] : ''; url += `&${k}=${encodeURIComponent(value)}`; } return url ? url.substring(1) : '';/* 這裏的substring保證不會有多餘的& */ }
另外,在jQuery中的AJAX中,已經封裝了JSONP,下面簡單介紹一下如何去使用。
$.ajax({ type: "get", url: "http://example.com", dataType: "jsonp", jsonp: "callback", jsonpCallback: "responseCallback", success: function (data) { console.log(data); }, error: function (data) { console.log(data); } });
在AJAX中,主要設置dataType
類型爲jsonp
。對於jsonp
參數來講,默認值是callback
,而jsonpCallback
參數的值默認是jQuery本身生成的。若是想本身指定一個回調函數,可像代碼中對jsonpCallback
進行設置。上面的代碼中,最終的url將會是http://example.com?callback=responseCallback
。
因爲同源策略僅存在於瀏覽器。對於服務器與服務器之間的通信,是不存在任何同源限制的說法的。
所以,使用代理服務器來轉發請求也是咱們在平常開發中解決跨域的一個經常使用的手段。
實現的方法很簡單,只要你會使用Node和Express。
須要注意的是,一般後臺服務器都會本身的一個驗證的機制,好比說微信文章在iframe
中圖片是加載不出來的,由於其後臺對referer
進行了驗證。另外,有些服務器也會經過發送一些uid
等等之類的字符串供後臺校驗。所以,咱們在使用代理服務器的時候,要重點關注請求的參數,這樣才能準確的模擬出請求並轉發。
下面簡單介紹如何使用代理服務器轉發請求。
最後,咱們來利用反微信圖片防盜鏈這個實例來寫一個代理服務器。
當咱們上線了一個網站的時候,而後img
標籤引用了微信圖片的地址,會出現下面的這種狀況。
這就是所謂的防盜鏈。
如今咱們給它加上一個代理,代碼以下:
var express = require("express"); var superagent = require("superagent"); var app = express(); app.use("/static", express.static("public")); app.get("/getwxImg", (req, res) => { //若是單純的去獲取會出現參數丟失的狀況,由於出現了兩個問號 var url = req.url.substring(req.url.indexOf("param=") + 6); res.writeHead(200, { 'Content-Type': 'image/*' }); superagent.get(url) .set('Referer', '') .set("User-Agent", 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36' ) .end(function (err, result) { if (err) { return false; } res.end(result.body); return; }); }); app.listen(4001, (err) => { if (err) { console.log(err); } else { console.log("server run!"); } });
這樣子,咱們就能夠把連接修改爲咱們服務器的地址,而後把真正的url做爲咱們的參數。
<!-- 這是有代理的狀況 --> <img src="http://localhost:4001/getwxImg?param=http://mmbiz.qpic.cn/mmbiz/CoJreiaicGKekEsuheJJ7Xh53AFe1BJKibyaQzsFiaxfHHdYibsHzfnicbcsj6yBmtYoJXxia9tFufsPxyn48UxiaccaAA/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&tp=webp"> <!-- 下面是沒有代理的狀況 --> <img src="http://mmbiz.qpic.cn/mmbiz/CoJreiaicGKekEsuheJJ7Xh53AFe1BJKibyaQzsFiaxfHHdYibsHzfnicbcsj6yBmtYoJXxia9tFufsPxyn48UxiaccaAA/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&tp=webp">
結果以下:
結果顯而易見,這就是所謂的代理服務器,附上github項目地址。
參考連接:
瀏覽器同源策略:https://developer.mozilla.org...
Cookie與Session:http://www.cnblogs.com/linguo...
CORS跨域資源共享:http://www.ruanyifeng.com/blo...