摘要:瀏覽器同源策略什么是瀏覽器同源策略同源策略是瀏覽器安全的基礎(chǔ)。同源策略限制從一個(gè)源加載的文檔或腳本如何與來(lái)自另一個(gè)源的資源進(jìn)行交互。這里我們做一下簡(jiǎn)要的總結(jié)受到瀏覽器同源策略的限制,頁(yè)面的無(wú)法被頁(yè)面的訪問(wèn)和操作。不受同源策略的限制。
瀏覽器同源策略 什么是瀏覽器同源策略?
“同源策略”(Same Origin Policy)是瀏覽器安全的基礎(chǔ)。
同源策略限制從一個(gè)源加載的文檔或腳本如何與來(lái)自另一個(gè)源的資源進(jìn)行交互。這是一個(gè)用于隔離潛在惡意文件的關(guān)鍵的安全機(jī)制。
在判斷兩個(gè)頁(yè)面的url是否具有相同的源之前,我們先來(lái)看一下一個(gè)url(統(tǒng)一資源定位符)的基本組成部分。
對(duì)于一個(gè)url,它的基本組成部分是:協(xié)議 :// 域名(或者是ip) : 端口(如果指定了) / 路徑。
那么下面我們來(lái)舉個(gè)例子:
對(duì)于http://www.example.com/static來(lái)說(shuō),協(xié)議就是http,它的域名是www.example.com,它的路徑是static,這里的端口號(hào)省略了(默認(rèn)為80)。
值得一提的是,在這個(gè)域名下面,example.com是主域名,而www.example.com是子域名,同樣的a.example.com也是一個(gè)與前面所述不同的另一個(gè)子域名。并且,上面這三個(gè)域名互不相同。
這里不再展開(kāi)贅述,相關(guān)知識(shí)請(qǐng)查閱與計(jì)算機(jī)網(wǎng)絡(luò)有關(guān)的知識(shí)。
那如何判斷頁(yè)面是否具有相同的源呢?
如果協(xié)議,端口(如果指定了)和域名對(duì)于兩個(gè)頁(yè)面是相同的,則兩個(gè)頁(yè)面具有相同的源。
也就是說(shuō),判斷同源的三要素是:協(xié)議、端口、域名。
需要注意的是,如果是一個(gè)域名二級(jí)域名,比如上面提到的www.example.com,它與另外一個(gè)二級(jí)域名a.example.com,雖然他們的主域相同,但是子域不同,于是這兩個(gè)就不是同一個(gè)域名,所以也不能說(shuō)是同源。三級(jí)域名依次類(lèi)推...
下表給出了相對(duì)http://store.company.com/dir/page.html同源檢測(cè)的示例:
關(guān)于Cookie和Session說(shuō)到同源策略,必不可少的就是Cookie這個(gè)東西了。
而講到Cookie,跟它關(guān)聯(lián)在一起的又有Session。
對(duì)于這兩者,這里不做大篇幅的介紹,具體去傳送門(mén)查閱。
這里我們做一下簡(jiǎn)要的總結(jié):
Cookie受到瀏覽器同源策略的限制,A頁(yè)面的Cookie無(wú)法被B頁(yè)面的Cookie訪問(wèn)和操作。
Cookie最大存儲(chǔ)容量一般為4KB,由服務(wù)器生成,在HTTP報(bào)文首部設(shè)置Set-Cookie可以指定生成Cookie的內(nèi)容和生命周期,如果由瀏覽器生成則在關(guān)掉瀏覽器后失效。
Cookie存儲(chǔ)在瀏覽器端,由于每次發(fā)送HTTP請(qǐng)求默認(rèn)會(huì)把Cookie附到HTTP首部上去,所以Cookie主要用來(lái)身份認(rèn)證,而不用來(lái)存儲(chǔ)其他信息,防止HTTP報(bào)文過(guò)大。
Session存儲(chǔ)在服務(wù)器,主要與Cookie配合使用完成身份認(rèn)證和狀態(tài)保持的功能。只有Cookie或只有Session都無(wú)法完成身份認(rèn)證和狀態(tài)保持的功能。
最后,對(duì)Cookie和Session實(shí)現(xiàn)的身份認(rèn)證和狀態(tài)保持功能做一個(gè)舉例。
假設(shè)現(xiàn)在有一個(gè)學(xué)生信息管理系統(tǒng),此時(shí)數(shù)據(jù)庫(kù)已經(jīng)有學(xué)生的相關(guān)信息。(賬號(hào)、密碼、個(gè)人信息等等)
然后當(dāng)學(xué)生登錄這個(gè)系統(tǒng),通過(guò)POST請(qǐng)求把用戶的賬戶密碼發(fā)送到后臺(tái)服務(wù)器。當(dāng)后臺(tái)服務(wù)器接收到這些參數(shù)的時(shí)候,會(huì)跟數(shù)據(jù)庫(kù)保存的記錄進(jìn)行匹配。
一旦匹配成功,也就是用戶的賬號(hào)密碼都正確的情況下,這個(gè)時(shí)候后臺(tái)服務(wù)器會(huì)在Session中記錄一個(gè)值,可以是用戶名或者其他能夠唯一標(biāo)識(shí)用戶的字段。
當(dāng)把這個(gè)值保存在Session中后,后臺(tái)服務(wù)器會(huì)返回響應(yīng)告知客戶端登錄成功,可以進(jìn)行后續(xù)的操作。此時(shí),后臺(tái)服務(wù)器會(huì)在HTTP響應(yīng)報(bào)文中添加一個(gè)字段Set-Cookie,它的值是當(dāng)前Session的SessionID,(這個(gè)SessionID是指向我們當(dāng)前的那個(gè)Session的,在Node的Express中express-session會(huì)封裝好這個(gè)過(guò)程)當(dāng)然還會(huì)設(shè)置Cookie的其他屬性,比如說(shuō)過(guò)期時(shí)間Expires等等。
當(dāng)瀏覽器接收到這個(gè)HTTP響應(yīng)報(bào)文的時(shí)候,就會(huì)在本地設(shè)置一個(gè)Cookie,它的過(guò)期時(shí)間由響應(yīng)報(bào)文中Set-Cookie中的Expires字段的值決定,如果為空,則關(guān)閉瀏覽器(即會(huì)話結(jié)束時(shí))后失效。
之后,每次向后臺(tái)服務(wù)器發(fā)送請(qǐng)求的時(shí)候,瀏覽器默認(rèn)會(huì)把這個(gè)Cookie加在HTTP請(qǐng)求報(bào)文的Cookie中。這樣,每次后臺(tái)服務(wù)器接收到請(qǐng)求的時(shí)候,會(huì)根據(jù)Cookie中的SessionID去找到我們的Session。
假如這個(gè)SessionID映射得到Session,那么這個(gè)時(shí)候說(shuō)明瀏覽器是已經(jīng)登錄過(guò)了。于是,就可以進(jìn)行后續(xù)的一些相關(guān)的操作。
另外,值得一提的是,Session機(jī)制決定了當(dāng)前客戶只會(huì)獲取到自己的Session,而不會(huì)獲取到別人的Session。各客戶的Session也彼此獨(dú)立,互不可見(jiàn)。也就是說(shuō),當(dāng)多個(gè)客戶端執(zhí)行程序時(shí),服務(wù)器會(huì)保存多個(gè)客戶端的Session。獲取Session的時(shí)候也不需要聲明獲取誰(shuí)的Session。
這就是Cookie和Session做狀態(tài)保持和身份驗(yàn)證的一個(gè)具體的例子。
關(guān)于iframe說(shuō)到同源限制,還有一個(gè)不得不提的就是iframe。
iframe可以在父頁(yè)面中嵌入一個(gè)子頁(yè)面,在日常開(kāi)發(fā)中一旦使用,避免不了的就要涉及到不同的iframe頁(yè)面進(jìn)行通信的問(wèn)題,可能是獲得其他iframe的DOM,或者是獲取其他iframe上的全局變量或方法等等。
同源下的iframe,也就是iframe中的src屬性的URL符合同源的條件,那么通過(guò)iframe的contentDocument和contentWindow獲取其他iframe的DOM或者全局變量、方法都是很簡(jiǎn)單的事情。
那如果是非同源的兩個(gè)iframe,單純的通過(guò)變量訪問(wèn)的方式就受到同源限制了。
為了解決這個(gè)問(wèn)題,HTML5引入了一個(gè)新的API:postMessage,主要就是用來(lái)解決存在跨域問(wèn)題的iframe頁(yè)面之間通信的問(wèn)題。
下面簡(jiǎn)單的舉一個(gè)例子,假如現(xiàn)在有兩個(gè)不同的頁(yè)面,A頁(yè)面的url是http://localhost:4002/parent.html,B頁(yè)面的url的是http://localhost:4003/child.html,現(xiàn)在我把B頁(yè)面用iframe嵌在A頁(yè)面下面,代碼(精簡(jiǎn))是這樣子的。現(xiàn)在我要實(shí)現(xiàn)的是向子頁(yè)面B傳遞一個(gè)消息:
A頁(yè)面代碼:
A頁(yè)面
B頁(yè)面代碼:
B頁(yè)面
結(jié)果如圖:
postMessage接受兩個(gè)參數(shù),一個(gè)是要傳送的data,另外一個(gè)是目標(biāo)窗口的源,如果想傳給任何窗口,可以設(shè)置成*。
目標(biāo)頁(yè)面接收信息的時(shí)候,使用的是window.addEventListener("message", function() {})。
當(dāng)然也有不受同源限制的情況存在,主要有以下列舉的:
script標(biāo)簽允許跨域嵌入腳本,稍后介紹的JSONP就是利用這個(gè)“漏洞”來(lái)實(shí)現(xiàn)。
img標(biāo)簽、link標(biāo)簽、@font-face不受跨域影響。
video和audio嵌入的資源。
iframe載入的任何資源。(不是iframe之間的通信)
、和的插件。
WebSocket不受同源策略的限制。
開(kāi)發(fā)時(shí)常用解決方案 CORS——跨域資源共享注:以下跨域資源共享的兩種請(qǐng)求辨析內(nèi)容摘抄至阮一峰的《跨域資源共享》一文。
什么是跨域資源共享?CORS是一個(gè)W3C標(biāo)準(zhǔn),全稱是“跨域資源共享”(Cross-origin resource sharing)。
它允許瀏覽器向跨源服務(wù)器發(fā)出XMLHttpRequest請(qǐng)求,從而克服了AJAX只能同源發(fā)送請(qǐng)求的限制。
實(shí)現(xiàn)CORS主要在于服務(wù)器的設(shè)置,關(guān)鍵在于服務(wù)器HTTP響應(yīng)報(bào)文首部的設(shè)置。前端部分大致還是跟原來(lái)發(fā)AJAX請(qǐng)求沒(méi)什么區(qū)別,只是需要對(duì)AJAX進(jìn)行一些相關(guān)的設(shè)置,稍后我們都會(huì)講到。
CORS的兩種請(qǐng)求在講解如何實(shí)現(xiàn)跨域資源共享的時(shí)候,我們先來(lái)看一下CORS的兩種請(qǐng)求。
瀏覽器將CORS分為兩種請(qǐng)求,一種是簡(jiǎn)單請(qǐng)求,另外一種對(duì)應(yīng)的肯定就是非簡(jiǎn)單請(qǐng)求。
只要同時(shí)滿足下面兩大條件,就屬于簡(jiǎn)單請(qǐng)求:
請(qǐng)求的方法是一下的三種方法之一:
HEAD
GET
POST
HTTP的頭信息不超過(guò)以下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type: 只限于三個(gè)值:application/x-www-form-urlencoded、multipart/formdata、text/plain。
凡是不同時(shí)滿足以上兩種條件,就屬于非簡(jiǎn)單請(qǐng)求。
瀏覽器對(duì)于兩種請(qǐng)求處理是不一樣的。
簡(jiǎn)單請(qǐng)求對(duì)于簡(jiǎn)單請(qǐng)求,瀏覽器直接發(fā)出CORS請(qǐng)求。具體來(lái)說(shuō),就是在HTTP請(qǐng)求報(bào)文首部,增加一個(gè)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字段的用來(lái)說(shuō)明本次請(qǐng)求來(lái)自哪個(gè)源(協(xié)議+域名+端口)。服務(wù)器根據(jù)這個(gè)值,決定是否同意這次請(qǐng)求。
如果Origin指定的源,不在許可范圍內(nèi),服務(wù)器會(huì)返回一個(gè)正常的HTTP回應(yīng)。瀏覽器發(fā)現(xiàn),這個(gè)回應(yīng)的頭信息沒(méi)有包含Access-Control-Allow-Origin字段(詳見(jiàn)下文),就知道出錯(cuò)了,從而拋出一個(gè)錯(cuò)誤,被XMLHttpRequest的onerror回調(diào)函數(shù)捕獲。注意,這種錯(cuò)誤無(wú)法通過(guò)狀態(tài)碼識(shí)別,因?yàn)镠TTP回應(yīng)的狀態(tài)碼有可能是200。
如果Origin指定的域名在許可范圍內(nèi),服務(wù)器返回的響應(yīng),會(huì)多出幾個(gè)頭信息字段。
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響應(yīng)報(bào)文首部信息中,有三個(gè)與CORS請(qǐng)求相關(guān)的字段,都是以Access-Control-開(kāi)頭。
Access-Control-Allow-Origin
該字段是必須的,它的值要么是請(qǐng)求Origin字段,要么是一個(gè)*,表示接受任意域名的請(qǐng)求。
Access-Control-Allow-Credentials
該字段可選。它的值是一個(gè)布爾值,表示是否允許發(fā)送Cookie。默認(rèn)情況下,Cookie不包括在CORS請(qǐng)求之中。設(shè)為true,即表示服務(wù)器明確許可,Cookie可以包含在請(qǐng)求中,一起發(fā)給服務(wù)器。這個(gè)值也只能設(shè)為true,如果服務(wù)器不要瀏覽器發(fā)送Cookie,刪除該字段即可。
值得一提的是,如果想要CORS支持Cookie,不僅要在服務(wù)器指定HTTP響應(yīng)報(bào)文首部字段,還需要在AJAX中打開(kāi)withCredentials的屬性。(jQuery中AJAX設(shè)置后面會(huì)講到)
var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
有些瀏覽器在省略withCredentials設(shè)置的時(shí)候,還是會(huì)發(fā)送Cookie。于是,可以顯式關(guān)閉這個(gè)屬性。
xhr.withCredentials = false;
需要注意的是,如果要發(fā)送Cookie,Acess-Control-Allow-Origin不能設(shè)置為*,必須設(shè)置成具體的域名,如果是本地調(diào)試的話可以考慮設(shè)置成null。
Access-Control-Expose-Headers
該字段可選。CORS請(qǐng)求時(shí),XMLHttpRequest對(duì)象的getResponseHeader()方法只能拿到6個(gè)基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必須在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader("FooBar")可以返回FooBar字段的值。
非簡(jiǎn)單請(qǐng)求是那種對(duì)服務(wù)器有特殊要求的請(qǐng)求,比如請(qǐng)求方法是PUT或DELETE,或者Content-Type字段的類(lèi)型是application/json。
非簡(jiǎn)單請(qǐng)求的CORS請(qǐng)求,會(huì)在正式通信之前,增加一次HTTP查詢請(qǐng)求,稱為"預(yù)檢"請(qǐng)求(preflight)。
瀏覽器先詢問(wèn)服務(wù)器,當(dāng)前網(wǎng)頁(yè)所在的域名是否在服務(wù)器的許可名單之中,以及可以使用哪些HTTP動(dòng)詞和頭信息字段。只有得到肯定答復(fù),瀏覽器才會(huì)發(fā)出正式的XMLHttpRequest請(qǐng)求,否則就報(bào)錯(cuò)。
下面是一段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();
很明顯,這是一個(gè)非簡(jiǎn)單請(qǐng)求,使用了PUT方法來(lái)發(fā)送請(qǐng)求,并且自定義了一個(gè)HTTP請(qǐng)求報(bào)文的首部字段。
于是,瀏覽器發(fā)現(xiàn)這是一個(gè)非簡(jiǎn)單的請(qǐng)求,就自動(dòng)發(fā)出了一個(gè)“預(yù)檢”請(qǐng)求,要求服務(wù)器確認(rèn)可以這樣請(qǐng)求。下面是這個(gè)“預(yù)檢”請(qǐng)求的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...
"預(yù)檢"請(qǐng)求用的請(qǐng)求方法是OPTIONS,表示這個(gè)請(qǐng)求是用來(lái)詢問(wèn)的。頭信息里面,關(guān)鍵字段是Origin,表示請(qǐng)求來(lái)自哪個(gè)源。
除了Origin字段,“預(yù)檢”請(qǐng)求的頭信息還包括兩個(gè)特殊字段。
Access-Control-Request-Method
該字段是必須的,用來(lái)列出瀏覽器的CORS會(huì)用到哪些HTTP方法,上面是PUT。
Access-Control-Request-Headers
該字段是一個(gè)用逗號(hào)分隔的字符串,指定瀏覽器CORS請(qǐng)求會(huì)額外發(fā)送的頭信息。上面的例子是X-Custom-Header。
于是,服務(wù)器收到“預(yù)檢”請(qǐng)求之后,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,確認(rèn)允許跨域請(qǐng)求,就可以做出回應(yīng)。
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
如果瀏覽器否定了"預(yù)檢"請(qǐng)求,會(huì)返回一個(gè)正常的HTTP回應(yīng),但是沒(méi)有任何CORS相關(guān)的頭信息字段。這時(shí),瀏覽器就會(huì)認(rèn)定,服務(wù)器不同意預(yù)檢請(qǐng)求,因此觸發(fā)一個(gè)錯(cuò)誤,被XMLHttpRequest對(duì)象的onerror回調(diào)函數(shù)捕獲。控制臺(tái)會(huì)打印出如下的報(bào)錯(cuò)信息。
XMLHttpRequest cannot load http://api.alice.com. Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
服務(wù)器回應(yīng)的其他CORS相關(guān)字段如下:
Access-Control-Allow-Methods: GET, POST, PUT Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000
對(duì)比簡(jiǎn)單請(qǐng)求服務(wù)器響應(yīng)的CORS字段,發(fā)現(xiàn)多了三個(gè):
Access-Control-Allow-Methods
該字段必需,它的值是逗號(hào)分隔的一個(gè)字符串,表明服務(wù)器支持的所有跨域請(qǐng)求的方法。注意,返回的是所有支持的方法,而不單是瀏覽器請(qǐng)求的那個(gè)方法。這是為了避免多次"預(yù)檢"請(qǐng)求。
Access-Control-Allow-Headers
如果瀏覽器請(qǐng)求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個(gè)逗號(hào)分隔的字符串,表明服務(wù)器支持的所有頭信息字段,不限于瀏覽器在"預(yù)檢"中請(qǐng)求的字段。
Access-Control-Max-Age
該字段可選,用來(lái)指定本次預(yù)檢請(qǐng)求的有效期,單位為秒。上面結(jié)果中,有效期是20天(1728000秒),即允許緩存該條回應(yīng)1728000秒(即20天),在此期間,不用發(fā)出另一條預(yù)檢請(qǐng)求。
于是,一旦瀏覽器通過(guò)了“預(yù)檢”,以后每次瀏覽器正常的CORS請(qǐng)求,都跟簡(jiǎn)單請(qǐng)求一樣,會(huì)有一個(gè)Origin頭信息字段。服務(wù)器的回應(yīng),也都有一個(gè)Access-Control-Allow-Origin頭信息字段。如果開(kāi)啟了Cookie設(shè)置,那還有一個(gè)Access-Control-Allow-Credentials:true。
如何在Node實(shí)現(xiàn)跨域資源共享?那怎么在Node中結(jié)合Express設(shè)置后臺(tái)的跨域部分呢?
其實(shí)很簡(jiǎn)單,需要設(shè)置的就是上面所述的幾個(gè)響應(yīng)首部的字段,主要考慮兩種類(lèi)型的請(qǐng)求和是否需要使用Cookie。具體設(shè)置如下:
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"); /* 服務(wù)器支持的所有字段 */ res.header("Access-Control-Allow-Credentials", "true"); /* 當(dāng)使用Cookie時(shí) */ res.header("Access-Control-Max-Age", 300000); /* 設(shè)置預(yù)檢請(qǐng)求的有效期 */ if (req.method === "OPTIONS") return res.send(200); /*讓options請(qǐng)求快速返回*/ else next(); });
上面的設(shè)置有幾個(gè)需要注意的地方:
如果需要本地調(diào)試,也就是在本地HTML頁(yè)面發(fā)請(qǐng)求(類(lèi)似file://...之類(lèi)的url),可以把Access-Control-Allwo-Origin的值設(shè)置為Null,這樣子就能夠使用Cookie。如果設(shè)置成*,雖然也可以跨域發(fā)送請(qǐng)求,但是這個(gè)時(shí)候沒(méi)有辦法使用Cookie。
Access-Control-Allow-Headers字段不是必須的,僅當(dāng)發(fā)送的請(qǐng)求有Access-Control-Request-Headers時(shí)需要設(shè)置。
使用Cookie的時(shí)候要配置Access-Control-Allow-Methods字段。
“預(yù)檢”請(qǐng)求可設(shè)置緩存時(shí)間,可以保證不多次發(fā)送“預(yù)檢”請(qǐng)求。
需要判斷是否為“預(yù)檢”請(qǐng)求,當(dāng)攔截“預(yù)檢”請(qǐng)求的時(shí)候直接返回。
如果使用jQuery封裝的AJAX發(fā)送請(qǐng)求,那么需要在相應(yīng)的JS代碼設(shè)置:
$.ajaxSetup({ xhrFields: { withCredentials: true }, crossDomain: true });
withCredentials是設(shè)置CORS發(fā)送Cookie,默認(rèn)是不發(fā)送的。
crossDomain告知AJAX允許跨域。
以上就是CORS設(shè)置跨域的具體介紹。
基于JSONP技術(shù)實(shí)現(xiàn)跨域 JSONP原理對(duì)于JSONP來(lái)說(shuō),前面也已經(jīng)提到了,其實(shí)它是利用了某些不受同源限制的標(biāo)簽的所謂“漏洞”,來(lái)實(shí)現(xiàn)“曲線救國(guó)”式的跨域的方案。
它借用script標(biāo)簽不受同源限制的這個(gè)特性,通過(guò)動(dòng)態(tài)的給頁(yè)面添加一個(gè)script標(biāo)簽,利用事先聲明好的數(shù)據(jù)處理函數(shù)來(lái)獲取數(shù)據(jù)。
值得一提的是,JSONP這種方法其實(shí)和CORS有很大的區(qū)別,它并不屬于一種規(guī)范。所謂的JSONP是應(yīng)用JSON數(shù)據(jù)的一種新方法,它只不過(guò)是被包含在函數(shù)調(diào)用中的JSON。
在JSONP中包含兩部分:回調(diào)函數(shù)和數(shù)據(jù)。其中,回調(diào)函數(shù)是當(dāng)響應(yīng)到來(lái)時(shí)要放在當(dāng)前頁(yè)面被調(diào)用的函數(shù)。而數(shù)據(jù),就是傳入回調(diào)函數(shù)中的JSON字符串,也就是回調(diào)函數(shù)的參數(shù)了。下面我們簡(jiǎn)單模擬一下JSONP的通信過(guò)程。
JSONP的原理詳細(xì)講解可以看這個(gè)傳送門(mén)。
我們來(lái)簡(jiǎn)單的模擬一下JSONP的通信過(guò)程。
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"})*/
它的過(guò)程是這樣子的:
當(dāng)我們通過(guò)新建一個(gè)script標(biāo)簽請(qǐng)求時(shí),后臺(tái)會(huì)根據(jù)相應(yīng)的參數(shù)來(lái)生成相應(yīng)的JSON數(shù)據(jù)。比如說(shuō)上面這個(gè)鏈接,傳遞了handleResponse給后臺(tái),然后后臺(tái)根據(jù)這個(gè)參數(shù)再結(jié)合數(shù)據(jù)生成了handleResponse({"data": "hey"})。
緊接著,這個(gè)返回的JSON數(shù)據(jù)其實(shí)就可以被當(dāng)成一個(gè)js腳本,就是對(duì)一個(gè)函數(shù)的調(diào)用。
由于我們事先已經(jīng)聲明了這么一個(gè)回調(diào)函數(shù),于是當(dāng)資源加載進(jìn)來(lái)的時(shí)候,直接就對(duì)函數(shù)進(jìn)行調(diào)用,于是數(shù)據(jù)當(dāng)然就能獲取到了。
至此,跨域通信完成。
另外,想要實(shí)現(xiàn)JSONP,后臺(tái)服務(wù)器也必須做相應(yīng)的設(shè)置。
值得一提的是,JSONP是存在一定的局限性的:
只能用于GET請(qǐng)求
存在安全問(wèn)題,請(qǐng)求代碼中可能存在安全隱患
要確定JSONP請(qǐng)求是否失敗并不容易
一個(gè)簡(jiǎn)單的JSONP實(shí)現(xiàn)下面是一個(gè)實(shí)現(xiàn)JSONP的庫(kù),我們來(lái)一起分析一下它的源代碼。這是源碼的github地址。
/** * Module dependencies */ var debug = require("debug")("jsonp"); //使用依賴 /** * Module exports. */ module.exports = jsonp; //輸出模塊 /** * Callback index. */ var count = 0; //回調(diào)函數(shù)的index值,便于取名。 /** * Noop function. */ function noop(){} //無(wú)操作空函數(shù),以便使用后把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是取得數(shù)據(jù)后的callback,不是傳給服務(wù)器的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; //一定時(shí)間內(nèi)后臺(tái)服務(wù)器沒(méi)有返回視為超時(shí) if (timeout) { timer = setTimeout(function(){ cleanup(); if (fn) fn(new Error("Timeout")); }, timeout); } //回復(fù)原始設(shè)置、清空狀態(tài) function cleanup(){ if (script.parentNode) script.parentNode.removeChild(script); window[id] = noop; if (timer) clearTimeout(timer); } //取消操作 function cancel(){ if (window[id]) { cleanup(); } } //聲明函數(shù),等待script標(biāo)簽加載的url引入完畢后調(diào)用 window[id] = function(data){ debug("jsonp got", data); cleanup(); if (fn) fn(null, data);//node中約定第一個(gè)參數(shù)為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標(biāo)簽后會(huì)直接去調(diào)用聲明的函數(shù),然后函數(shù)會(huì)把script標(biāo)簽帶有的data給傳出去 return cancel; //返回初始狀態(tài) }基于JSONP庫(kù)的封裝
接著,我們可以利用上面的這個(gè)庫(kù),給它進(jìn)行一個(gè)封裝,下面是我們自己寫(xiě)的_jsonp函數(shù):
/* 這個(gè)是自己定義的一個(gè)_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封裝成了一個(gè)promise對(duì)象,回調(diào)函數(shù)中如果成功的話會(huì)把數(shù)據(jù)帶回來(lái)然后resolve出去 */ } //緊接著是對(duì)參數(shù)的一個(gè)序列化 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保證不會(huì)有多余的& */ }在jQuery中使用JSONP
另外,在jQuery中的AJAX中,已經(jīng)封裝了JSONP,下面簡(jiǎn)單介紹一下如何去使用。
$.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中,主要設(shè)置dataType類(lèi)型為jsonp。對(duì)于jsonp參數(shù)來(lái)說(shuō),默認(rèn)值是callback,而jsonpCallback參數(shù)的值默認(rèn)是jQuery自己生成的。如果想自己指定一個(gè)回調(diào)函數(shù),可像代碼中對(duì)jsonpCallback進(jìn)行設(shè)置。上面的代碼中,最終的url將會(huì)是http://example.com?callback=responseCallback。
使用代理服務(wù)器轉(zhuǎn)發(fā)請(qǐng)求由于同源策略僅存在于瀏覽器。對(duì)于服務(wù)器與服務(wù)器之間的通訊,是不存在任何同源限制的說(shuō)法的。
因此,使用代理服務(wù)器來(lái)轉(zhuǎn)發(fā)請(qǐng)求也是我們?cè)谌粘i_(kāi)發(fā)中解決跨域的一個(gè)常用的手段。
實(shí)現(xiàn)的方法很簡(jiǎn)單,只要你會(huì)使用Node和Express。
需要注意的是,通常后臺(tái)服務(wù)器都會(huì)自己的一個(gè)驗(yàn)證的機(jī)制,比如說(shuō)微信文章在iframe中圖片是加載不出來(lái)的,因?yàn)槠浜笈_(tái)對(duì)referer進(jìn)行了驗(yàn)證。另外,有些服務(wù)器也會(huì)通過(guò)發(fā)送一些uid等等之類(lèi)的字符串供后臺(tái)校驗(yàn)。因此,我們?cè)谑褂么矸?wù)器的時(shí)候,要重點(diǎn)關(guān)注請(qǐng)求的參數(shù),這樣才能準(zhǔn)確的模擬出請(qǐng)求并轉(zhuǎn)發(fā)。
下面簡(jiǎn)單介紹如何使用代理服務(wù)器轉(zhuǎn)發(fā)請(qǐng)求。
分析url請(qǐng)求所需要的參數(shù)。
代理服務(wù)器暴露出一個(gè)api,這個(gè)路由實(shí)際的功能是去請(qǐng)求真正的服務(wù)器。
之后,我們只要請(qǐng)求這個(gè)api,我們所建的代理服務(wù)器就會(huì)默認(rèn)的幫我們?nèi)マD(zhuǎn)發(fā)請(qǐng)求到真正的服務(wù)器上,其中會(huì)加上一些相應(yīng)的參數(shù)。
最后,我們來(lái)利用反微信圖片防盜鏈這個(gè)實(shí)例來(lái)寫(xiě)一個(gè)代理服務(wù)器。
如何使用代理服務(wù)器反微信圖片防盜鏈?當(dāng)我們上線了一個(gè)網(wǎng)站的時(shí)候,然后img標(biāo)簽引用了微信圖片的地址,會(huì)出現(xiàn)下面的這種情況。
這就是所謂的防盜鏈。
現(xiàn)在我們給它加上一個(gè)代理,代碼如下:
var express = require("express"); var superagent = require("superagent"); var app = express(); app.use("/static", express.static("public")); app.get("/getwxImg", (req, res) => { //如果單純的去獲取會(huì)出現(xiàn)參數(shù)丟失的情況,因?yàn)槌霈F(xiàn)了兩個(gè)問(wèn)號(hào) 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!"); } });
這樣子,我們就可以把鏈接修改成我們服務(wù)器的地址,然后把真正的url作為我們的參數(shù)。
結(jié)果如下:
結(jié)果顯而易見(jiàn),這就是所謂的代理服務(wù)器,附上github項(xiàng)目地址。
參考鏈接:
瀏覽器同源策略:https://developer.mozilla.org...
Cookie與Session:http://www.cnblogs.com/linguo...
CORS跨域資源共享:http://www.ruanyifeng.com/blo...
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/90146.html
摘要:在接觸前端開(kāi)發(fā)起,跨域這個(gè)詞就一直以很高的頻率在我們學(xué)習(xí)工作中重復(fù)出現(xiàn),最近在工作中遇到了跨域的相關(guān)問(wèn)題,這里我把它總結(jié)記錄一下。 在接觸前端開(kāi)發(fā)起,跨域這個(gè)詞就一直以很高的頻率在我們學(xué)習(xí)工作中重復(fù)出現(xiàn),最近在工作中遇到了跨域的相關(guān)問(wèn)題,這里我把它總結(jié)記錄一下。關(guān)于跨域,有N種類(lèi)型,現(xiàn)在我只專注于ajax請(qǐng)求跨域(ajax跨域只是屬于瀏覽器同源策略中的一部分,其它的這里不做介紹),內(nèi)容...
摘要:關(guān)于,強(qiáng)烈推薦閱讀跨域資源共享詳解阮一峰另外,這里也整理了一個(gè)實(shí)現(xiàn)原理圖簡(jiǎn)化版如何判斷是否是簡(jiǎn)單請(qǐng)求瀏覽器將請(qǐng)求分成兩類(lèi)簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求。 前言 從剛接觸前端開(kāi)發(fā)起,跨域這個(gè)詞就一直以很高的頻率在身邊重復(fù)出現(xiàn),一直到現(xiàn)在,已經(jīng)調(diào)試過(guò)N個(gè)跨域相關(guān)的問(wèn)題了,16年時(shí)也整理過(guò)一篇相關(guān)文章,但是感覺(jué)還是差了點(diǎn)什么,于是現(xiàn)在重新梳理了一下。 個(gè)人見(jiàn)識(shí)有限,如有差錯(cuò),請(qǐng)多多見(jiàn)諒,歡迎提出iss...
摘要:需注意的是由于同源策略的限制,所讀取的為跨域請(qǐng)求接口所在域的,而非當(dāng)前頁(yè)。目前,所有瀏覽器都支持該功能需要使用對(duì)象來(lái)支持,也已經(jīng)成為主流的跨域解決方案。反向代理接口跨域跨域原理同源策略是瀏覽器的安全策略,不是協(xié)議的一部分。 什么是跨域? 跨域是指一個(gè)域下的文檔或腳本試圖去請(qǐng)求另一個(gè)域下的資源,這里跨域是廣義的。 廣義的跨域: 1.) 資源跳轉(zhuǎn): A鏈接、重定向、表單提交 2.) 資源...
摘要:用于告知瀏覽器可以將預(yù)先檢查請(qǐng)求返回結(jié)果緩存的時(shí)間,在緩存有效期內(nèi),瀏覽器會(huì)使用緩存的預(yù)先檢查結(jié)果判斷是否發(fā)送跨域請(qǐng)求。 跨域,老生常談的問(wèn)題 簡(jiǎn)述 作為一只前端菜鳥(niǎo),跨域方面只懂得JSONP和CORS,并未曾深入了解。但隨著春招越來(lái)越近,就算是菜鳥(niǎo)也要猛振翅膀。近幾日仔細(xì)研究了跨域問(wèn)題,寫(xiě)下這篇文章,希望對(duì)開(kāi)發(fā)者們有所幫助。在讀本文前,希望您對(duì)以下知識(shí)略有了解。 瀏覽器同源策略 n...
摘要:上節(jié)我們講了同源策略,這節(jié)我們講講如何跨域。當(dāng)這些從的腳本執(zhí)行出錯(cuò),因?yàn)檫`背了同源策略為了保證用戶信息不被泄露,錯(cuò)誤信息不會(huì)顯示出來(lái),取而代之只會(huì)返回一個(gè)。 前端最基礎(chǔ)的就是 HTML+CSS+Javascript。掌握了這三門(mén)技術(shù)就算入門(mén),但也僅僅是入門(mén),現(xiàn)在前端開(kāi)發(fā)的定義已經(jīng)遠(yuǎn)遠(yuǎn)不止這些。前端小課堂(HTML/CSS/JS),本著提升技術(shù)水平,打牢基礎(chǔ)知識(shí)的中心思想,我們開(kāi)課啦(每...
閱讀 3596·2023-04-26 02:24
閱讀 931·2023-04-25 14:47
閱讀 2478·2021-11-24 11:16
閱讀 1711·2021-11-24 09:38
閱讀 1571·2021-11-18 10:07
閱讀 2061·2021-09-22 15:49
閱讀 1589·2019-08-30 15:55
閱讀 875·2019-08-26 13:38