理解跨域及常用解決方案
跨域,相信大家無論是在工作中還是在面試中經常遇到這個問題,常常在網上看到別人所整理的一些方法,看似知道是怎麼回事,但如果沒有動手實踐過,總覺得自己沒有真正的掌握,在這裡,通過自己認真思考整理一些常用的方法。
跨域的產生
不用多講,作為一名前端開發人員,相信大家都知道跨域是因為瀏覽器的同源策略所導致的。所謂同源是指"協議+域名+埠"三者相同,即便兩個不同的域名指向同一個ip地址,也非同源。瀏覽器引入同源策略主要是為了防止XSS,CSRF攻擊。
CSRF(Cross-site request forgery),跨站請求偽造,也被稱為:one click attack/session riding,縮寫為:CSRF/XSRF。
在同源策略影響下,域名A向域名B傳送Ajax請求,或操作Cookie、LocalStorage、indexDB等資料,或操作dom,js就會受到限制,但請求css,js等靜態資源不受限制
跨域的解決方案
1 通過jsonp跨域
首先說一下jsonp的原理,例如我們平時寫html的時候常常會使用
<script src="www.b.com/js/jquery.js"></script>這種方式去取放在另外伺服器上的靜態資源,這個是不受同源策略所限制的,所以我們利用這一點可以解決跨域的問題。
主要程式碼如下:
1.1原生實現
在www.a.com域名寫下如下程式碼,去請求www.b.com域名的資料 <script> var script = document.creatElement('script'); script.type = 'text/javascript'; script.src = 'http://www.b.com/getdata?callback=demo'; function demo(res){ console.log(res); } </script>
這裡,我們利用動態指令碼的src屬性,變相地傳送了一個http://www.b.com/getdata?call...。這時候,b.com頁面接受到這個請求時,如果沒有JSONP,會正常返回json的資料結果,像這樣:{ msg: 'helloworld' },而利用JSONP,服務端會接受這個callback引數,然後用這個引數值包裝要返回的資料:demo({msg: 'helloworld'});
這時候,如果a.com的頁面上正好有一個demo 的函式:
function demo(res){
console.log(res);
}
當遠端資料一返回的時候,隨著動態指令碼的執行,這個demo函式就會被執行。
1.2 jquery ajax請求實現
$.ajax({
url:'http://www.b.com/getdata',
type:'get',
dataType: 'jsonp', // 請求方式為jsonp
jsonpCallback: 'demo', // 自定義回撥函式名
data: {}
});
服務端程式碼實現:
以nodejs為例
var http = require(http);
//引入url模組解析url字串
var url = require('url);
//引入querystring模組處理query字串
var querystring = require('querystring');
var server = http.createServer();
server.on('request',function(req,res){
var urlPath = url.parse(req.url).pathname;
var param = querystring .parse(req.url.split('?')[1]);
if(urlPath === '/getData' && param.callback) {
res.writeHead(200,{'Content-Type','application/json;charset=utf-8'});
var data = { msg: 'helloworld' };
data = JSON.stringify(data );
var callback = param .callback+'('+data+');';
res.write(callback);
res.end();
} else {
res.writeHead(200, {'Content-Type':'text/html;charset=utf-8'});
res.write('Hell World\n');
res.end();
}
})
jsonp缺點:只能使用get請求,不推薦使用
2 CORS 跨域資源共享
跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓執行在一個 origin (domain) 上的Web應用被准許訪問來自不同源伺服器上的指定的資源。當一個資源從與該資源本身所在的伺服器不同的域或埠請求一個資源時,資源會發起一個跨域 HTTP 請求。Cross-Origin Resource Sharing跨域資源共享,應該算是現在比較推薦的跨域處理方案.不僅適用於各種Method,而且更加方便和簡單
目前,所有瀏覽器都支援該功能,IE瀏覽器不能低於IE10。
2.1 簡單請求和非簡單請求
瀏覽器將CORS請求分成兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。
簡單請求同時滿足以下條件,只要不滿足以下條件的則為非簡單請求
非簡單請求會發出一次預檢測請求,返回碼是204,預檢測通過才會真正發出請求,這才返回200。這裡通過前端發請求的時候增加一個額外的headers來觸發非簡單請求。
2.2 進行帶有身份憑證的CORS 請求
- 預設情況下的跨域請求都是不會把cookie傳送給伺服器的,在需要傳送的情況下,如果是xhr,那麼需要設定xhr.withCredentials=true,
- 如果是採用fetch獲取的話,那麼需要在request裡面設定 credentials:'include',
- 但是如果伺服器在預請求的時候沒返回Access-Control-Allow-Crenditials:true的話,那麼在實際請求的時候,cookie是不會被髮送給伺服器端的,要特別注意對於簡單的get請求,不會有預請求的過程,
- 那麼在實際請求的時候,如果伺服器沒有返回Access-Control-Allow-Crenditials:true的話那麼響應結果瀏覽器也不會交給請求者
對於附帶身份憑證的請求,伺服器不得設定 Access-Control-Allow-Origin 的值為“*”。這是因為請求的首部中攜帶了 Cookie 資訊,如果 Access-Control-Allow-Origin
的值為“*”,請求將會失敗。而將 Access-Control-Allow-Origin 的值設定為
http://www.a.com,則請求將成功執行。
2.3 HTTP 響應首部欄位
- Access-Control-Allow-Origin: <origin> | *
- Access-Control-Expose-Headers 頭讓伺服器把允許瀏覽器訪問的頭放入白名單
- Access-Control-Max-Age 頭指定了preflight請求的結果能夠被快取多久
- Access-Control-Allow-Credentials
頭指定了當瀏覽器的credentials設定為true時是否允許瀏覽器讀取response的內容。 - Access-Control-Allow-Methods 首部欄位用於預檢請求的響應。其指明瞭實際請求所允許使用的 HTTP 方法。
- Access-Control-Allow-Headers 首部欄位用於預檢請求的響應。其指明瞭實際請求中允許攜帶的首部欄位。
2.4 以nodejs express為例,說明如何使用cors解決跨域
var express=require('express');
var url=require('url');
var app=express();
var allowCrossDomain = function(req, res, next) {
res.header('Access-Control-Allow-Origin', 'http://localhost:63342');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type');
res.header('Access-Control-Allow-Credentials','true');
next();
};
app.use(allowCrossDomain);
app.get('/getData',function (req,res,next) {
var queryValue=url.parse(req.url).query;
if(queryValue==='[email protected]'){
res.send(true);
}else {
res.send(false);
}
});
app.listen(3001);
實際開發過程中,為了安全,會和token一起使用
3 window.postMessage
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是為數不多可以跨域操作的window屬性之一,它可用於解決以下方面的問題:
- iframe巢狀頁面跨域通訊
- 頁面和其開啟的新視窗的通訊
- 多視窗之間訊息傳遞
用法:
postMessage(data,origin)方法接受兩個引數,
data:需要傳遞的資料,html5規範支援任意基本型別或可複製的物件,但部分瀏覽器只支援字串,所以傳參時最好用JSON.stringify()序列化。
origin:協議+主機+埠號,也可以設定為"*",表示可以傳遞給任意視窗,如果要指定和當前視窗同源的話設定為"/"。
程式碼示例:
http://www.a.com/a.html
<iframe id="iframe" src="http://www.b.com/b.html" style="display:none;"></iframe>
<script>
var iframe = document.getElementById('iframe');
iframe.onload = function() {
var data = {
name: 'jianjian'
};
// 向http://www.b.com傳送跨域資料
iframe.contentWindow.postMessage(JSON.stringify(data),'http://www.b.com');
};
// 接受http://www.b.com返回資料
window.addEventListener('message', function(e) {
alert('data from http://www.b.com---> ' + e.data);
}, false);
</script>
<script>
// 接收http://www.a.com/a.html的資料
window.addEventListener('message', function(e) {
alert('data from http://www.a.com/a.html---> ' + e.data);
var data = JSON.parse(e.data);
if (data) {
data.number = 16;
// 處理後再發回http://www.a.com/a.html
window.parent.postMessage(JSON.stringify(data), 'http://www.a.com');
}
}, false);
</script>
4 document.domain
這種方式只適合主域名相同,但子域名不同的iframe跨域。
實現原理:兩個頁面都通過js強制設定document.domain為基礎主域,就實現了同域。
<iframe id="iframe" src="http://www.child.a.com/b.html" style="display:none;"></iframe>
<script>
document.domain = 'a.com';
var a = 'hello world';
</script>
"http://www.child.a.com/b.html
<script>
document.domain = 'a.com';
var b = window.parent.a;
console.log(b);
</script>
5 window.name
window.name 傳輸技術的基本原理:
當在瀏覽器中開啟一個頁面,或者在頁面中新增一個iframe時即會建立一個對應的window物件,當頁面載入另一個新的頁面時,window.name的屬性是不會變的。這樣就可以利用在頁面動態新增一個iframe然後載入資料頁面,在資料頁面將需要的資料賦值給window.name。然而此時承載的iframe的parent頁面還是不能直接訪問不在同一域下的iframe的那麼屬性,這時,只需要將iframe再載入一個與承載頁面同域的空白頁面,即可對window.name進行資料讀取。
通過iframe的src屬性由外域轉向本地域,跨域資料即由iframe的window.name從外域傳遞到本地域。這個就巧妙地繞過了瀏覽器的跨域訪問限制,但同時它又是安全操作。
具體實現:
http://www.a.com/a.html 主頁面
http://www.b.com/b.html 資料頁面
http://www.a.com/proxy.html 代理頁面
<script>
function crosDomainGetData(url,callback){
var state = 0;
var iframe = document.createElement('iframe);
iframe.src = url;
iframe.onload = function(){
if(state === 1){
//代理頁面成功過後,讀取window.name
var data = iframe.contentWindow.name;
callback&&callback(data);
//銷燬iframe
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else {
//第一次載入資料頁面成功後,切換代理頁面
state = 1;
iframe.contentWindow.location = 'http://www.a.com/proxy.html';
}
}
document.body.appendChild(iframe);
}
crosDomainGetData('http://www.b.com/b.html',function(data){
alert(data);
})
</script>
window.name = '123'
6 nginx代理跨域
server{
# 監聽8080埠
listen 8080;
# 域名是localhost
server_name localhost;
#凡是localhost:8080/api這個樣子的,都轉發到真正的服務端地址http://www.b.com:8080
location ^~ /api {
proxy_pass http://www.b.com:8080;
}
}
配置之後就不需要前端做什麼修改了,一般我們在前後端分離專案中開發階段會採用這種方式,但不是所有場景都能這樣做,例如後端介面是一個公共的API,比如一些公共服務獲取天氣什麼的。
7 WebSocket協議跨域
websoket協議天然支援跨域,你只需要學會如何使用它即可,關於websocket協議請看我的另外一篇文章WebSocket網路通訊協議
參考文章:
https://developer.mozilla.org...
https://segmentfault.com/a/11...
原文地址:https://segmentfault.com/a/1190000017312269