前端跨域問題
瀏覽器的同源策略
提到跨域不能不先說一下”同源策略”悯姊。
何為同源渊涝?只有當(dāng)協(xié)議渣慕、端口贵试、和域名都相同的頁面,則兩個(gè)頁面具有相同的源当辐。只要網(wǎng)站的 協(xié)議名protocol、 主機(jī)host、 端口號(hào)port 這三個(gè)中的任意一個(gè)不同哪廓,網(wǎng)站間的數(shù)據(jù)請(qǐng)求與傳輸便構(gòu)成了跨域調(diào)用,會(huì)受到同源策略的限制初烘。
同源策略限制從一個(gè)源加載的文檔或腳本如何與來自另一個(gè)源的資源進(jìn)行交互涡真。這是一個(gè)用于隔離潛在惡意文件的關(guān)鍵的安全機(jī)制。瀏覽器的同源策略肾筐,出于防范跨站腳本的攻擊哆料,禁止客戶端腳本(如 JavaScript)對(duì)不同域的服務(wù)進(jìn)行跨站調(diào)用(通常指使用XMLHttpRequest請(qǐng)求)。
跨域請(qǐng)求方式
解決跨域問題吗铐,最簡(jiǎn)單的莫過于通過nginx反向代理
進(jìn)行實(shí)現(xiàn)东亦,但是其需要在運(yùn)維層面修改,且有可能請(qǐng)求的資源并不再我們控制范圍內(nèi)(第三方)唬渗,所以該方式不能作為通用的解決方案典阵,下面闡述了經(jīng)常用到幾種跨域方式:
首先奋渔,先設(shè)置一下環(huán)境來調(diào)試跨域問題
1、利用node.js作為服務(wù)端壮啊,關(guān)于搭建就用 node express 快速搭建
<figure style="margin: 1em 0px;"><figcaption style="margin-top: 0.66667em; padding: 0px 1em; font-size: 0.9em; line-height: 1.5; text-align: center; color: rgb(153, 153, 153);">生成一個(gè)測(cè)試應(yīng)用demo</figcaption>
</figure>
2嫉鲸、本地配置域名
打開hosts,授予管理權(quán)限并打開編輯歹啼,加入
127.0.0.1 www.mynodetest.com
127.0.0.1 child.mynodetest.com
打開終端玄渗,cd進(jìn)入myapp目錄,執(zhí)行
node start
在publish文件中創(chuàng)建index.html文件
<figure style="margin: 1em 0px;"></figure>
然后打開瀏覽器染突,執(zhí)行 www.mytestnode.com/index.html 就能訪問 index.html頁面
<figure style="margin: 1em 0px;">如果網(wǎng)頁提示域名解析錯(cuò)誤捻爷,那么就是本地網(wǎng)絡(luò)設(shè)置了固定ip,dns份企,代理的問題
① 將internet協(xié)議版本如(TCP/IPv4)中的固定值設(shè)置為自動(dòng)獲取也榄,同時(shí)取消代理
</figure>
<figure style="margin: 1.6em 0px 1em;"></figure>
<figure style="margin: 1em 0px;">② 代理中直接加入過濾 mynodetest.com
</figure>
基本調(diào)試環(huán)境設(shè)置完畢,開始嘗試跨域方式
一司志、jsonp (只能支持get請(qǐng)求)
打開 myapp/routes/index.js甜紫,加入一個(gè)jsonp請(qǐng)求
<figure style="margin: 1em 0px;"></figure>
同時(shí)在html也加入一個(gè)script
<figure style="margin: 1em 0px;"></figure>
刷新瀏覽器,地址還是 www.mynodetest.com/index.html
<figure style="margin: 1em 0px;"></figure>
二骂远、document.domain + iframe
僅限主域相同囚霸,子域不同的應(yīng)用場(chǎng)景
瀏覽器訪問的地址是 http://www.mynodetest.com:3000/index.html
嵌入iframe的地址是 http://child.mynodetest.com:3000/test.html
都用js手動(dòng)設(shè)置 document.domain = 'mynodetest.com'; 作為基礎(chǔ)域,繼而實(shí)現(xiàn)同域
myapp/publish 新增test.html激才, 并加入以下代碼
<figure style="margin: 1em 0px;"></figure>
index.html中加入 user拓型, 并使用iframe嵌入 http://child.mynodetest.com:3000/test.html
<figure style="margin: 1em 0px;"></figure>
好了,此時(shí)重新刷新頁面瘸恼,在控制臺(tái)能看見跨域的問題了劣挫,
<figure style="margin: 1em 0px;"></figure>
把test.html中注釋的地方取消注釋,在刷新頁面
<figure style="margin: 1em 0px;"></figure>
就能看到在index.html中定義的 user
三东帅、location.has + iframe
a 與 b 不同域压固,但是相互之間要進(jìn)行通信,可以通過中間頁 c 與(a 同域)實(shí)現(xiàn)靠闭,不同域使用hash傳值帐我,相同域使用js訪問
1、 a.html (http://www.mynodetest.com:3000/a.html)
<body>
<iframe id='iframe' src="http://child.mynodetest.com:3000/b.html" frameborder="0"></iframe>
<button id='button'>set b.html #user</button>
</body>
<script>
document.getElementById('button').onclick = () => {
var iframe = document.getElementById('iframe');
iframe.src = `${iframe.src}#user=mynodetest`;
}
function cb(result) {
console.log(result);
}
</script>
2愧膀、b.html (http://child.mynodetest.com:3000/b.html)
<body>
<iframe id='iframe' src="http://www.mynodetest.com:3000/c.html" frameborder="0"></iframe>
</body>
<script>
var iframe = document.getElementById('iframe');
window.onhashchange = () => {
iframe.src = `${iframe.src}${location.hash}`;
}
</script>
3拦键、c.html (http://www.mynodetest.com:3000/c.html)
<script>
window.onhashchange = () => {
window.parent.parent.cb(location.hash.replace('#user=', ''));
}
</script>
四、window.name + iframe
window.name 在不用的地址(甚至不同域名)加載后依舊存在于該窗口扇调,上限 2M
通過 iframe.src 外域轉(zhuǎn)向本地域矿咕,跨域數(shù)據(jù)由 iframe 的 window.name 從外域傳到本地域,繞過瀏覽器的訪問限制,同時(shí)又是安全操作
1碳柱、a.html (http://www.mynodetest.com:3000/a.html)
const proxy = (url, cb) => {
let state = 0;
let iframe = document.createElement('iframe');
iframe.src = url;
iframe.onload = () => {
if(state === 0) {
iframe.contentWindow.location = 'http://www.mynodetest.com:3000/c.html';
state = 1;
} else if(state === 1) {
cb(iframe.contentWindow.name);
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
}
}
document.body.appendChild(iframe);
}
proxy('http://child.mynodetest.com:3000/b.html', name => console.log(name));
2捡絮、c.html (http://www.mynodetest.com:3000/c.html)
沒什么內(nèi)容,主要的作用是與 a 作用域相同
3莲镣、b.html (http://child.mynodetest.com:3000/b.html)
window.name = 'b.html'
五福稳、postMessage
postMessage是 HTML5 XMLHttpRequest Level 2中的API, 支持瀏覽器跨域操作
用法 postMessage( data, origin )
data:html5 規(guī)范支持任意基本類型或可復(fù)制對(duì)象瑞侮,但部分瀏覽器只支持字符串的圆,可用 JSON.stringify() 序列化。
origin:協(xié)議 + 主機(jī) + 端口號(hào)半火,可以設(shè)置為 "*"越妈,表示可以傳遞給任意窗口,要指定和當(dāng)前窗口同源設(shè)置為 "/"钮糖。
- 頁面和其它打開的新窗口的數(shù)據(jù)傳遞
- 多窗口之間的信息傳遞
- 頁面與嵌套iframe消息傳遞
- 以上三個(gè)場(chǎng)景的跨域數(shù)據(jù)傳遞
⑴ a.html (http://www.mynodetest.com:3000/a.html)
<iframe id="iframe" src="http://child.mynodetest.com:3000/b.html"></iframe>
let iframe = document.getElementById('iframe');
iframe.onload = () => {
const data = { name: 'a to b' };
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://child.mynodetest.com');
}
window.addEventListener('message', e => {
console.log(e.data);
})
⑵ b.html (http://child.mynodetest.com:3000/b.html)
window.addEventListener('message', e => {
console.log(e.data);
window.parent.postMessage('b to a ', 'http://www.mynodetest.com');
})
六梅掠、跨域資源共享 (CORS)
普通跨域請(qǐng)求: 只需要服務(wù)端設(shè)置 Access-Control-Allow-Origin
帶cookie請(qǐng)求:前后端都需要設(shè)置值,所帶的cookie為跨域請(qǐng)求接口所在域的cookie
1店归、前端請(qǐng)求
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('post', 'http://child.mynodetest.com:3000/post', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');
xhr.onreadystatechange = function() {
if(xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
2阎抒、node (express)
router.post('/post', function(req, res) {
//req.headers.origin
res.header("Access-Control-Allow-Origin", 'http://www.mynodetest.com:3000'); //需要顯示設(shè)置來源
//res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
//res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("Access-Control-Allow-Credentials",true); //帶cookies
//res.header("X-Powered-By",' 3.2.1')
//res.header("Content-Type", "application/json;charset=utf-8");
res.header('Set-Cookie', 'l=a123456;Path=/;Domain=child.mynodetest.com;HttpOnly');
res.json({ test: '22222' });
});
七、nginx代理跨域
1消痛、nginx配置解決iconfont跨域
瀏覽器跨域訪問js且叁、css、img等常規(guī)靜態(tài)資源被同源策略許可秩伞,但iconfont字體文件(eot|otf|ttf|woff|svg)例外逞带,可在nginx的靜態(tài)資源服務(wù)器中加入以下配置。
location / { add_header Access-Control-Allow-Origin *; }
2纱新、nginx反向代理接口跨域
跨域原理:同源策略是瀏覽器的安全策略掰担,不是http協(xié)議的一部分。服務(wù)端調(diào)用http接口只是http協(xié)議怒炸,不會(huì)執(zhí)行js腳本,不需要同源策略毡代,也不存在跨域問題阅羹。
實(shí)現(xiàn)思路:通過nginx配置一個(gè)代理服務(wù)器(域名與domain1相同,端口不同)教寂,反向代理訪問domain2接口捏鱼,可以順便修改cookie中domain信息,方便當(dāng)前域cookie寫入酪耕,實(shí)現(xiàn)跨域
假設(shè)當(dāng)前有兩個(gè)域名 www.domain1.com www.domain2.com
#proxy服務(wù)器
server {
listen 81;
server_name www.my;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 當(dāng)用webpack-dev-server等中間件代理接口訪問nignx時(shí)导梆,此時(shí)無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啟用
add_header Access-Control-Allow-Origin http://www.domain1.com; #當(dāng)前端只跨域不帶cookie時(shí)看尼,可為*
add_header Access-Control-Allow-Credentials true;
}
}
⑴ 前端
var xhr = new XMLHttpRequest();
// 前端開關(guān):瀏覽器是否讀寫cookie
xhr.withCredentials = true;
// 訪問nginx中的代理服務(wù)器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
⑵ node
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
var params = qs.parse(req.url.substring(2));
// 向前臺(tái)寫cookie
res.writeHead(200, {
'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly' // HttpOnly:腳本無法讀取
});
res.write(JSON.stringify(params));
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
八递鹉、node中間件代理
與nginx相同,通過啟動(dòng)一個(gè)代理服務(wù)器藏斩,實(shí)時(shí)數(shù)據(jù)轉(zhuǎn)發(fā)
1躏结、前端
var xhr = new XMLHttpRequest();
// 前端開關(guān):瀏覽器是否讀寫cookie
xhr.withCredentials = true;
// 訪問http-proxy-middleware代理服務(wù)器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
2、node中間件
var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
// 代理跨域目標(biāo)接口
target: 'http://www.domain2.com:8080',
changeOrigin: true,
// 修改響應(yīng)頭信息狰域,實(shí)現(xiàn)跨域并允許帶cookie
onProxyRes: function(proxyRes, req, res) {
res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
res.header('Access-Control-Allow-Credentials', 'true');
},
// 修改響應(yīng)信息中的cookie域名
cookieDomainRewrite: 'www.domain1.com' // 可以為false媳拴,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');
九、websocket協(xié)議
WebSocket protocol是HTML5一種新的協(xié)議兆览。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信屈溉,同時(shí)允許跨域通訊,是server push技術(shù)的一種很好的實(shí)現(xiàn)抬探。原生WebSocket API使用起來不太方便子巾,我們使用http://Socket.io,它很好地封裝了webSocket接口驶睦,提供了更簡(jiǎn)單砰左、靈活的接口,也對(duì)不支持webSocket的瀏覽器提供了向下兼容场航。
1缠导、前端
<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');
// 連接成功處理
socket.on('connect', function() {
// 監(jiān)聽服務(wù)端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});
// 監(jiān)聽服務(wù)端關(guān)閉
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});
document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>
2、node socket
var http = require('http');
var socket = require('socket.io');
// 啟http服務(wù)
var server = http.createServer(function(req, res) {
res.writeHead(200, {
'Content-type': 'text/html'
});
res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');
// 監(jiān)聽socket連接
socket.listen(server).on('connection', function(client) {
// 接收信息
client.on('message', function(msg) {
client.send('hello:' + msg);
console.log('data from client: ---> ' + msg);
});
// 斷開處理
client.on('disconnect', function() {
console.log('Client socket has closed.');
});
});