瀏覽器在請求不一樣域的資源時,會由於同源策略的影響請求不成功,這就是一般被提到的「跨域問題」。做爲前端開發,解決跨域問題應該是一個被熟練掌握的技能。而隨着技術不斷的更迭,針對跨域問題的解決也衍生出了多種解決方案。咱們一般會根據項目的不一樣須要,而採起不一樣的方式。這篇文章,將詳細總結跨域問題的相關知識點,以便在遇到相同問題的時候,能有一個清晰的解決思路。javascript
早期爲了防止CSRF(跨域請求僞造)的攻擊,瀏覽器引入了同源策略(SOP)來提升安全性。css
CSRF(Cross-site request forgery),跨站請求僞造,也被稱爲:one click attack/session riding,縮寫爲:CSRF/XSRF。 —— 淺談CSRF攻擊方式html
而所謂"同源策略",即同域名(domain或ip)、同端口、同協議的才能互相獲取資源,而不能訪問其餘域的資源。在同源策略影響下,一個域名A的網頁能夠獲取域名B下的腳本,css,圖片等,可是不能發送Ajax請求,也不能操做Cookie、LocalStorage等數據。同源策略的存在,一方面提升了網站的安全性,但同時在面對先後端分離、模擬測試等場景時,也帶來了一些麻煩,從而不得不尋求一些方法來突破限制,獲取資源。前端
這裏所說的JS跨域,指的是在處理跨域請求的過程當中,技術面會偏瀏覽器端較多一些,通常是利用瀏覽器的一些特性進行hack處理,從而避開同源策略的限制。java
因爲同源策略不會阻止動態腳本的插入到文檔中去,因此催生出了一種很經常使用的跨域方式: JSONP(JSON with Padding)。node
原理提及來也很簡單:react
假設,咱們源頁面是在a.com,想要獲取b.com的數據,咱們能夠動態插入來源於b.com的腳本:git
script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'http://www.b.com/getdata?callback=demo';複製代碼
這裏,咱們利用動態腳本的src屬性,變相地發送了一個http://www.b.com/getdata?callback=demo
的GET請求。這時候,b.com頁面接受到這個請求時,若是沒有JSONP,會正常返回json的數據結果,像這樣:github
{ msg: 'helloworld' }複製代碼
而利用JSONP
,服務端會接受這個callback
參數,而後用這個參數值包裝要返回的數據:ajax
demo({msg: 'helloworld'});複製代碼
這時候,若是a.com的頁面上正好有一個demo的函數:
function demo(data) {
console.log(data.msg);
}複製代碼
當遠程數據一返回的時候,隨着動態腳本的執行,這個demo函數就會被執行。
到這裏,你應該能明白這個技術爲何叫JSONP了吧?就是由於使用這種技術服務器會接受回調函數名做爲請求參數,並將JSON數據填充進回調函數中去。
不過通常在實際開發的時候,咱們通常會利用jQuery對JSONP的支持,而避免手寫不少代碼。從1.2版本開始,jQuery中加入了對JSONP的支持,可使用$.getJSON
方法來請求跨域數據:
//callback後面的?會由jQuery自動生成方法名
$.getJSON('http://www.b.com/getdata?callback=?', function(data) {
console.log(data.msg);
});複製代碼
還有一種更加經常使用的方法是,利用$.ajax
方法,只要指定dataType
爲jsonp
便可:
$.ajax({
url: 'http://www.b.com/getdata?callback=?', //不指定回調名,可省略callback參數,會由jQuery自動生成
dataType: 'jsonp',
jsonpCallback: 'demo', //可省略
success: function(data) {
console.log(data.msg);
}
});複製代碼
雖然JSONP在跨域ajax請求方面有很強的能力,可是它也有一些缺陷。首先,它沒有關於JSONP調用的錯誤處理,一旦回調函數調用失敗,瀏覽器會以靜默失敗的方式處理。其次,它只支持GET請求,這是因爲該技術自己的特性所決定的。所以,對於一些須要對安全性有要求的跨域請求,JSONP的使用須要謹慎一點了。
因爲JSONP對於老瀏覽器兼容性方面比較良好,所以,對於那些對IE8如下仍然須要支持的網站來講,仍然被普遍應用。不過,針對高級瀏覽器,建議仍是使用接下來會介紹的CORS方法。
目前,不少大型網站都會使用多個子域名,而瀏覽器的同源策略對於它們來講就有點過於嚴格了。如,來自www.a.com想要獲取document.a.com中的數據。只要基礎域名相同,即可以經過修改document.domain爲基礎域名的方式來進行通訊,可是須要注意的是協議和端口也必須相同。
document.a.com中經過設置
document.domain = 'a.com';複製代碼
www.a.com中:
document.domain = 'a.com';
var iframe = document.createElement('iframe');
iframe.src = 'http://document.a.com';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function() {
var targetDocument = iframe.contentDocument || iframe.contentWindow.document;
//能夠操做targetDocument
}複製代碼
最後,推薦一個使用iframe跨域的庫github.com/jpillora/xd…
window.name這個全局屬性主要是用來獲取和設置窗口名稱的,可是經過結合iframe也能夠跨域獲取數據。咱們知道,每一個iframe都有包裹它的window對象,而這個window是最外層窗口的子對象。因此window.name屬性就能夠被共享。
下面這個簡單的例子,展現了a.com域名下獲取b.com域名下的數據:
var iframe = document.createElement('iframe');
var canGetData = false;
//監聽加載事件
iframe.onload = function() {
if (!canGetData) {
//修改爲同源
iframe.src = 'http://www.a.com';
canGetData = true;
} else {
var data = iframe.contentWindow.name;
//獲取數據後清除iframe,防止不斷刷新
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
}
iframe.src = 'http://www.b.com/getdata.html';
iframe.style.display = 'none';
document.body.appendChild(iframe);複製代碼
b.com/getdata.html中要存放的數據須要存儲在window.name屬性中:
<script> var data = {msg: 'hello, world'}; window.name = JSON.stringify(data); //name屬性只支持字符串,支持最大2MB的數據 </script>複製代碼
還有一種iframe結合location.hash的方式,跟該方法十分相似:是經過檢測iframe的src的hash屬性來傳遞數據的。因爲該方法相應速度較慢,這裏就不作介紹了。
window.name+iframe的方法曾經被做爲比JSONP更加安全的替代方案,然而對於託管敏感數據的現代Web應用程序來講,已經不推薦使用window.name來進行跨域消息傳遞了,而是推薦使用接下來介紹的postMessage
API。
postMessage
是HTML5新增在window對象上的方法,目的是爲了解決在父子頁面上通訊的問題。該技術有個專有名詞:跨文檔消息(cross-document messaging)。利用postMessage
的特性能夠實現較爲安全可信的跨域通訊。
postMessage
方法接受兩個參數:
下面介紹一個例子:
首先,先建立一個demo的html文件,咱們這裏採用的是iframe的跨域,固然也能夠跨窗口。
<p>
<button id="sendMsg">sendMsg</button>
</p>
<iframe id="receiveMsg" src="http://b.html">
</iframe>複製代碼
而後,在sendMsg
的按鈕上綁定點擊事件,觸發postMessage
方法來發送信息給iframe:
window.onload = function() {
var receiveMsg = document.getElementById('receiveMsg').contentWindow; //獲取在iframe中顯示的窗口
var sendBtn = document.getElementById('sendMsg');
sendBtn.addEventListener('click', function(e) {
e.preventDefault();
receiveMsg.postMessage('Hello world', 'http://b.html');
});
}複製代碼
接着,你須要在iframe的綁定的頁面源中監聽message
事件就能正常獲取消息了。其中,MessageEvent
對象有三個重要屬性:data
用於獲取數據,source
用於獲取發送消息的窗口對象,origin
用於獲取發送消息的源。
window.onload = function() {
var messageBox = document.getElementById('messageBox');
window.addEventListener('message', function(e) {
//do something
//考慮安全性,須要判斷一下信息來源
if(e.origin !== 'http://xxxx') return;
messageBox.innerHTML = e.data;
});
}複製代碼
總得來講,postMessage
的使用十分簡單,在處理一些和多頁面通訊、頁面與iframe等消息通訊的跨域問題時,有着很好的適用性。
在實踐過程當中,通常咱們喜歡讓服務器來多作一些處理,從而儘量讓前端簡化。這裏將介紹兩種經常使用的方法:反向代理和CORS。
所謂反向代理服務器,它是代理服務器中的一種。客戶端直接發送請求給代理服務器,而後代理服務器會根據客戶端的請求,從真實的資源服務器中獲取資源返回給客戶端。因此反向代理就隱藏了真實的服務器。利用這種特性,咱們能夠經過將其餘域名的資源映射成本身的域名來規避開跨域問題。
下面我將以node.js所寫的服務器來作一個演示:
const http = require('http');
const server = http.createServer((req, res) => {
const proxy_req = http.request({
port: 8080,
host: req.headers['host'],
method: req.method,
path: req.url,
headers: req.headers
});
proxy_req.on('response', proxy_res => {
proxy_res.on('data', data => {
res.write(data, 'binary');
});
proxy_res.on('end', () => {
res.end();
});
res.writeHead(proxy_res.statusCode, proxy_res.headers);
});
req.on('end', () => {
proxy_req.end();
});
req.on('data', data => {
proxy_req.write(data, 'binary');
});
});
server.listen(80);複製代碼
以上代碼會將請求80端口的資源映射到8080端口上去。原理就是在監聽到客戶端請求後,啓動一個代理服務器,而後獲取代理服務器返回的結果,直接返回給客戶端。
若是你使用的是express, 則代碼量將更少,也很方便:
const express = require('express');
const request = require('request');
const app = express();
const proxyServer = 'localhost:8080';
app.use('/', (req, res) => {
const url = proxyServer + req.url;
req.pipe(request(url)).pipe(res);
});
app.listen(process.env.PORT || 80);複製代碼
利用反向代理,你能夠將任何請求委託給另外的服務器,從而避免在瀏覽器端進行跨域操做。不過你須要注意的是:不要使用bodyParser
中間件,由於你須要直接將原始請求經過管道傳輸到外部服務器。
通常來講,若是你的生產環境上應用和API在同一臺服務器上運行,就沒有必要使用跨域了。 而在開發階段採用這種反向代理,則更加方便咱們前端開發和測試。
在使用反向代理上,你也能夠藉助node-http-proxy庫來減小代碼量。
"跨域資源共享"(Cross-origin resource sharing)是W3C出的一個標準。兼容性方面能夠支持IE8+(IE8和IE9須要使用XDomainRequest對象來支持CORS),因此如今CORS也已經成爲主流的跨域解決方案。
CORS的核心思想是經過一系列新增的HTTP頭信息來實現服務器和客戶端之間的通訊。因此,要支持CORS,服務端都須要作好相應的配置,這樣,在保證安全性的同時也更方便了前端的開發。
瀏覽器會將CORS請求分爲兩類:簡單請求和非簡單請求:
在CORS標準中,會根據是否觸發CORS preflight(預請求)來區分簡單請求和非簡單請求。
簡單請求須要知足如下幾個條件:
1.請求方法只容許:GET,HEAD,POST
2.對於請求頭字段有嚴格的要求,通常狀況下不會超過如下幾個字段:
3.當發起POST請求時,只容許Content-Type
爲application/x-www-form-urlencoded
,multipart/form-data
,text/plain
。
對於簡單請求來講,服務器和客戶端之間的通訊只是進行簡單的交換。如圖:
瀏覽器發送一個帶有Orgin
字段的HTTP請求頭,用來代表請求來源。服務器的Access-Control-Allow-Origin
響應頭代表該服務器容許哪些源的訪問,一旦不匹配,瀏覽器就會拒絕資源的訪問。大部分狀況,你們都喜歡將Access-Control-Allow-Origin
設置爲*
,即任意外域都能訪問該資源。可是,仍是推薦作好訪問控制,以保證安全性。
對於非簡單請求,狀況就稍微複雜了點。在正式發送請求數據以前,瀏覽器會先發送一個帶有'OPTIONS'方法的請求來確保該請求對於目標站點來講是安全的,這個請求也被稱爲」預請求「(preflight)。
瀏覽器和服務器之間具體的交互過程如圖所示:
瀏覽器會在預檢請求中,多發送兩個字段Access-Control-Request-Method
和Access-Control-Request-Headers
,前者用於告知服務器實際請求所用的方法,後者用於告知服務器實際請求所攜帶的自定義請求首部字段。而後,服務器將根據請求頭的信息來判斷是否容許該請求。
針對非簡單請求,服務器端能夠設置幾個相關字段:
另外,若是是在具體的實踐過程當中,調試OPTIONS請求可使用
curl -X OPTIONS http://xxx.com複製代碼
來進行查看相應頭信息。也能夠經過chrome://net-internals/#events
來獲取更加詳細的網絡請求信息。
針對非簡單請求來講,因爲每一個請求都會發送預請求,這就致使接口數據的返回會有所延遲,時間被加長。因此,在使用CORS的過程當中,能夠採用一些方案來優化請求,將非簡單請求轉換成簡單請求,從而提升請求的速度。
1.請求緩存
能夠在服務器端使用Access-Control-Max-Age
來緩存預請求的結果。從而提升網站性能。可是須要注意的是,大部分瀏覽器不會容許緩存‘OPTIONS‘請求太長時間,如:火狐是24小時(86400s),chromium是10分鐘(600s)。
2.針對GET請求
對於GET請求,不必使用Content-Type
, 儘量地保持GET請求是簡單請求。這樣就能夠減小Header上所攜帶的字段。從安全性上考慮,全部的API調用應該儘量使用https
協議,而這樣能夠將一些受權認證信息(如token)直接放在url中去,而沒必要放在頭部。
3.針對POST請求
對於POST請求,咱們能夠儘可能使用FormData
這種原生的格式:
function sendQuery(url, postData) {
let formData = new FormData();
for(var key in postData) {
formData.append(key, postData.key);
}
return fetch(url, {
body: formData,
headers: {
'Accept': '*/*'
},
method: 'POST'
});
}
sendQuery('http://www.xxx.com', {msg: 'hello'}).then(function(response) {
//do something with response
});複製代碼
CORS預請求會將用戶的身份認證憑據排除在外,包括cookie、http-authentication報頭等。若是須要支持用戶憑證,須要在XHR的withCredentials屬性設置爲true,同時Access-control-allow-origin
不能設置爲*
。在服務器端會利用響應報頭Access-Control-Allow-Credentials
來聲明是否支持用戶憑證。
同時,利用withCredentials
這個屬性,也能夠檢測瀏覽器是否支持CORS。下面建立一個帶有兼容性處理的cors請求:
function createCORSRequest(method, url){
var xhr = new XMLHttpRequest();
if ("withCredentials" in xhr){
xhr.open(method, url, true);
} else if (typeof XDomainRequest != "undefined"){
xhr = new XDomainRequest();
xhr.open(method, url);
} else {
xhr = null;
}
return xhr;
}
var request = createCORSRequest("POST", "http://www.xxx.com");
if (request){
request.onload = function(){
//do something with request.responseText
};
request.send();
}複製代碼
若是瀏覽器支持fetch,則使用它作跨域請求更加方便:
fetch('http://www.xxx.com', {
method: 'POST',
mode: 'cors',
credentials: 'include' //接受憑證
}).then(function(response) {
//do something with response
});複製代碼
以上介紹的這些跨域方法,可能有些已經不多使用了,可是這些方法在解決問題的思路上都有着必定的參考意義。因此當面對不可避免的跨域問題的時候,也但願這篇文章對你能有所幫助。