前言javascript
先後端處理數據交互時每每會遇到跨域的問題,那麼什麼是跨域? 有哪些跨域方式? 出現跨域又該如何解決呢?html
1、什麼是跨域?前端
理解跨域首先要理解同源策略,它是瀏覽器對js施加的一種安全限制,若是缺乏了同源策略,瀏覽器很容易受到XSS、CSFR、SQL注入等攻擊。所謂同源是指協議、域名、端口必須相同。瀏覽器在請求數據時都要遵循同源策略,那麼凡是發送請求的URL中協議、域名、端口三者之中的一點不一樣時,就叫作跨域。
java
而在同源策略當中,它所限制的內容就有如下幾個:node
可是如下三個標籤是容許跨域加載資源的:web
常見的跨域場景上面的圖表已經說了,在協議,域名,端口號三者中,假若其中之一不相同都會致使跨域的現象.ajax
在此特別說明的兩點:express
第一:若是是協議和端口形成的跨域問題「前臺」是無能爲力的。json
第二:在跨域問題上,僅僅是經過「URL的首部」來識別而不會根據域名對應的IP地址是否相同來判斷。「URL的首部」能夠理解爲「協議, 域名和端口必須匹配」。後端
這裏你或許有個疑問:請求跨域了,那麼請求到底發出去沒有?
跨域並非請求發不出去,請求能發出去,服務端能收到請求並正常返回結果,只是結果被瀏覽器攔截了。你可能會疑問明明經過表單的方式能夠發起跨域請求,爲何 Ajax 就不會?由於歸根結底,跨域是爲了阻止用戶讀取到另外一個域名下的內容,Ajax 能夠獲取響應,瀏覽器認爲這不安全,因此攔截了響應。可是表單並不會獲取新的內容,因此能夠發起跨域請求。同時也說明了跨域並不能徹底阻止 CSRF,由於請求畢竟是發出去了。
一. CORS
CORS是一個W3C標準,全稱是"跨域資源共享"(Cross-origin resource sharing)。它容許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。
基本上目前全部的瀏覽器都實現了CORS標準,其實目前幾乎全部的瀏覽器ajax請求都是基於CORS機制的,只不過可能平時前端開發人員並不關心而已(因此說其實如今CORS解決方案主要是考慮後臺該如何實現的問題)。
CORS 須要瀏覽器和後端同時支持目前,全部瀏覽器都支持該功能,IE瀏覽器不能低於IE10, IE 8 和 9 須要經過 XDomainRequest 來實現。
對於開發者來講,CORS通訊與同源的AJAX通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感受。
瀏覽器會自動進行 CORS 通訊,實現 CORS 通訊的關鍵是後端。只要後端實現了 CORS,就實現了跨域。
服務端設置 Access-Control-Allow-Origin 就能夠開啓 CORS。 該屬性表示哪些域名能夠訪問資源,若是設置通配符則表示全部網站均可以訪問資源。
經過這種方式解決跨域會出現兩種請求: 分別是簡單請求和複雜請求.
(1) 請求方法是如下三種方法之一:
(2)HTTP的頭信息不超出如下幾種字段:
|
凡是不一樣時知足上面兩個條件,就屬於非簡單請求。
瀏覽器對這兩種請求的處理,是不同的。
下面就來仔細談談簡單請求的基本流程:
1.1基本流程
對於簡單請求,瀏覽器直接發出CORS請求。具體來講,就是在頭信息之中,增長一個Origin
字段。
下面是一個例子,瀏覽器發現此次跨源AJAX請求是簡單請求,就自動在頭信息之中,添加一個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
指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段。
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... |
上面的頭信息之中,有三個與CORS請求相關的字段,都以Access-Control-
開頭。
該字段是必須的。它的值要麼是請求時Origin
字段的值,要麼是一個 *
,表示接受任意域名的請求。
該字段可選。它的值是一個布爾值,表示是否容許發送Cookie。默認狀況下,Cookie不包括在CORS請求之中。設爲true
,即表示服務器明確許可,Cookie能夠包含在請求中,一塊兒發給服務器。這個值也只能設爲true
,若是服務器不要瀏覽器發送Cookie,刪除該字段便可。
該字段可選。CORS請求時,XMLHttpRequest
對象的getResponseHeader()
方法只能拿到6個基本字段:Cache-Control
、Content-Language
、Content-Type
、Expires
、Last-Modified
、Pragma
。若是想拿到其餘字段,就必須在Access-Control-Expose-Headers
裏面指定。上面的例子指定,getResponseHeader('FooBar')
能夠返回FooBar
字段的值。
1.2 withCredentials 屬性
上面說到,CORS請求默認不發送Cookie和HTTP認證信息。若是要把Cookie發到服務器,一方面要服務器贊成,指定Access-Control-Allow-Credentials
字段。
Access-Control-Allow-Credentials: true
另外一方面,開發者必須在AJAX請求中打開withCredentials
屬性。
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
不然,即便服務器贊成發送Cookie,瀏覽器也不會發送。或者,服務器要求設置Cookie,瀏覽器也不會處理。
可是,若是省略withCredentials
設置,有的瀏覽器仍是會一塊兒發送Cookie。這時,能夠顯式關閉withCredentials
。
xhr.withCredentials = false;
須要注意的是,若是要發送Cookie,Access-Control-Allow-Origin
就不能設爲星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用服務器域名設置的Cookie纔會上傳,其餘域名的Cookie並不會上傳,且(跨源)原網頁代碼中的document.cookie
也沒法讀取服務器域名下的Cookie。
不符合以上條件的請求就確定是複雜請求了。
複雜請求的CORS請求,會在正式通訊以前,增長一次HTTP查詢請求,稱爲"預檢"請求,該請求是 option 方法的,經過該請求來知道服務端是否容許跨域請求。
咱們用PUT向後臺請求時,屬於複雜請求,後臺需作以下配置:
// 容許哪一個方法訪問我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 預檢的存活時間
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS請求不作任何處理
if (req.method === 'OPTIONS') {
res.end()
}
// 定義後臺返回的內容
app.put('/getData', function(req, res) {
console.log(req.headers)
res.end('小學生多多指教,我是小芳妞和小帆仔!')
})
接下來咱們看下一個完整複雜請求的例子,而且介紹下CORS請求相關的字段
// index.html let xhr = new XMLHttpRequest() document.cookie = 'name=xiamen' // cookie不能跨域 xhr.withCredentials = true // 前端設置是否帶cookie xhr.open('PUT', 'http://localhost:4000/getData', true) xhr.setRequestHeader('name', 'xiamen') xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(xhr.response) //獲得響應頭,後臺需設置Access-Control-Expose-Headers console.log(xhr.getResponseHeader('name')) } } } xhr.send() //server1.js let express = require('express'); let app = express(); app.use(express.static(__dirname)); app.listen(3000); //server2.js let express = require('express') let app = express() let whitList = ['http://localhost:3000'] //設置白名單 app.use(function(req, res, next) { let origin = req.headers.origin if (whitList.includes(origin)) { // 設置哪一個源能夠訪問我 res.setHeader('Access-Control-Allow-Origin', origin) // 容許攜帶哪一個頭訪問我 res.setHeader('Access-Control-Allow-Headers', 'name') // 容許哪一個方法訪問我 res.setHeader('Access-Control-Allow-Methods', 'PUT') // 容許攜帶cookie res.setHeader('Access-Control-Allow-Credentials', true) // 預檢的存活時間 res.setHeader('Access-Control-Max-Age', 6) // 容許返回的頭 res.setHeader('Access-Control-Expose-Headers', 'name') if (req.method === 'OPTIONS') { res.end() // OPTIONS請求不作任何處理 } } next() }) app.put('/getData', function(req, res) { console.log(req.headers) res.setHeader('name', 'jw') //返回一個響應頭,後臺需設置 res.end('哈嘍,我是小芳妞!') }) app.get('/getData', function(req, res) { console.log(req.headers) res.end('哈嘍,我是小帆仔!') }) app.use(express.static(__dirname)) app.listen(4000)
上述代碼由http://127.0.0.1:3000/index.html
向http://127.0.0.1:4000/
跨域請求,正如咱們上面所說的,後端是實現 CORS 通訊的關鍵。
二.JSONP方式解決跨域問題
jsonp解決跨域問題是一個比較古老的方案(實際中不推薦使用),這裏作簡單介紹(實際項目中若是要使用JSONP,通常會使用JQ等對JSONP進行了封裝的類庫來進行ajax請求)
實現原理:
JSONP之因此可以用來解決跨域方案,主要是由於Web頁面上調用js文件時則不受是否跨域的影響(不只如此,咱們還發現凡是擁有」src」這個屬性的標籤都擁有跨域的能力,好比<\script>、<\img>、<\iframe>)。
若是想經過純web端跨域訪問數據,能夠在遠程服務器上設法把數據裝進js格式的文件裏,供客戶端調用和進一步處理。
實現流程:
1.簡單實現
假設遠程服務器http://remoteserver.com根目錄下有個remote.js文件代碼以下:
alert('我是遠程文件');
本地服務器http://localserver.com下有個jsonp.html頁面代碼以下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript" src="http://remoteserver.com/remote.js"></script> </head> <body> </body> </html>
毫無疑問,頁面將會彈出一個提示窗體,顯示跨域調用成功。
2.如今咱們在jsonp.html頁面定義一個函數,而後在遠程remote.js中傳入數據進行調用。
jsonp.html頁面代碼以下:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript"> var localHandler = function(data){ alert('我是本地函數,能夠被跨域的remote.js文件調用,遠程js帶來的數據是:' + data.result); }; </script> <script type="text/javascript" src="http://remoteserver.com/remote.js"></script> </head> <body> </body> </html>
remote.js文件代碼以下:
localHandler({"result":"我是遠程js帶來的數據"});
運行以後查看結果,頁面成功彈出提示窗口,顯示本地函數被跨域的遠程js調用成功,而且還接收到了遠程js帶來的數據。
很欣喜,跨域遠程獲取數據的目的基本實現了,可是又一個問題出現了,我怎麼讓遠程js知道它應該調用的本地函數叫什麼名字呢?畢竟是jsonp的服務者都要面對不少服務對象,而這些服務對象各自的本地函數都不相同啊?咱們接着往下看。
3.聰明的開發者很容易想到,只要服務端提供的js腳本是動態生成的就好了唄,這樣調用者能夠傳一個參數過去告訴服務端 「我想要一段調用XXX函數的js代碼,請你返回給我」,因而服務器就能夠按照客戶端的需求來生成js腳本並響應了。
看jsonp.html頁面的代碼:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <script type="text/javascript"> // 獲得航班信息查詢結果後的回調函數 var flightHandler = function(data){ alert('你查詢的航班結果是:票價 ' + data.price + ' 元,' + '餘票 ' + data.tickets + ' 張。'); }; // 提供jsonp服務的url地址(無論是什麼類型的地址,最終生成的返回值都是一段javascript代碼) var url = "http://flightQuery.com/jsonp/flightResult.aspx?code=CA1998&callback=flightHandler"; // 建立script標籤,設置其屬性 var script = document.createElement('script'); script.setAttribute('src', url); // 把script標籤加入head,此時調用開始 document.getElementsByTagName('head')[0].appendChild(script); </script> </head> <body> </body> </html>
此次的代碼變化比較大,再也不直接把遠程js文件寫死,而是編碼實現動態查詢,而這也正是jsonp客戶端實現的核心部分,本例中的重點也就在於如何完成jsonp調用的全過程。
咱們看到調用的url中傳遞了一個code參數,告訴服務器我要查的是CA1998次航班的信息,而callback參數則告訴服務器,個人本地回調函數叫作flightHandler,因此請把查詢結果傳入這個函數中進行調用。
OK,服務器很聰明,這個叫作flightResult.aspx的頁面生成了一段這樣的代碼提供給jsonp.html
(服務端的實現這裏就不演示了,與你選用的語言無關,說到底就是拼接字符串):
flightHandler({
"code": "CA1998",
"price": 1780,
"tickets": 5
});
使用注意
基於JSONP的實現原理,因此JSONP只能是「GET」請求,不能進行較爲複雜的POST和其它請求,因此遇到那種狀況,就得參考下面的CORS解決跨域了(因此現在它也基本被淘汰了)
注意,因爲接口代理是有代價的,因此這個僅是開發過程當中進行的。
與前面的方法不一樣,前面CORS是後端解決,而這個主要是前端對接口進行代理,也就是:
關於如何實現代理,這裏就不重點描述了,方法不少,也不難,基本都是基於node.js的。
搜索關鍵字node.js
,代理請求
便可找到一大票的方案。
Access-Control-Max-Age:
這個頭部加上後,能夠緩存這次請求的秒數。
在這個時間範圍內,全部同類型的請求都將再也不發送預檢請求而是直接使用這次返回的頭做爲判斷依據。
很是有用,能夠大幅優化請求次數