前言:跨域與同源策略
跨域:通常出現(xiàn)在Web開發(fā)中,特別是在涉及到Ajax請(qǐng)求或Fetch API調(diào)用時(shí),當(dāng)一個(gè)網(wǎng)頁(yè)嘗試從不同的源加載資源時(shí),就會(huì)遇到跨域問(wèn)題。這里所說(shuō)的“不同的源”,是指請(qǐng)求資源的源(由協(xié)議、域名和端口號(hào)組成)與提供資源的源不一致。
http:// 192.168.3.1 :3000 /home
協(xié)議(http), 域名(192.168.3.1), 端口(3000), 路徑(/home)
同源策略:是為了保護(hù)用戶的隱私和數(shù)據(jù)安全,如果沒(méi)有同源策略,惡意網(wǎng)站可以通過(guò)腳本非法獲取其他網(wǎng)站上的敏感數(shù)據(jù),所以瀏覽器會(huì)通過(guò)實(shí)施同源策略來(lái)限制不同源之間的直接通信。同時(shí),也有些特別的情況是不受同源策略限制的,比如:
img標(biāo)簽下的
link標(biāo)簽下的
script標(biāo)簽下的
一:JSONP實(shí)現(xiàn)同源
-
-
借助script標(biāo)簽的src屬性不受同源策略的影響來(lái)發(fā)送請(qǐng)求
-
-
給后端攜帶一個(gè)參數(shù) callback 并在前端定義 callback 函數(shù)體
-
-
后端返回 callback 的調(diào)用形式并將要響應(yīng)的值作為 callback 函數(shù)的參數(shù)
-
-
當(dāng)瀏覽器接收到響應(yīng)后,就會(huì)觸發(fā)全局的 callback 函數(shù),從而讓 callback 以參數(shù)的形式接收后端的響應(yīng)
-
前端代碼
<script>
function jsonp(url, cb) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
window[cb] = function(data) {
resolve(data);
};
script.src = `${url}?cb=${cb}`;
document.body.appendChild(script);
});
}
jsonp('http://localhost:3000', 'callback').then(res => {
console.log(res);
});
</script>
后端代碼
const http = require('http');
http.createServer(function(req, res) {
const query = new URL(req.url, `http://${req.headers.host}`).searchParams;
if (query.get('cb')) {
const cb = query.get('cb');
const data = 'hello world';
const result = `${cb}("${data}")`;
res.end(result);
}
}).listen(3000);
$
但使用這種方法實(shí)現(xiàn)同源有兩個(gè)缺點(diǎn):
二:cors實(shí)現(xiàn)同源
核心思想是后端通過(guò)Access-Control-Allow-Origin
設(shè)置響應(yīng)頭來(lái)指定允許的域名,以此來(lái)通知瀏覽器此時(shí)同源策略不生效
前端代碼
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:3000');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
</script>
后端代碼
const http = require('http');
http.createServer((req, res) => {
res.writeHead(200, {
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
});
res.end('Hello World');
}).listen(3000);
同樣,也可以設(shè)置Access-Control-Allow-methods
來(lái)設(shè)置相應(yīng)的請(qǐng)求方法,post
,get
等等
三:proxy代理
-
- 前端應(yīng)用將原本需要跨域訪問(wèn)的請(qǐng)求發(fā)送給自身的后端服務(wù)器
-
- 后端服務(wù)器再將請(qǐng)求轉(zhuǎn)發(fā)至實(shí)際的目標(biāo)服務(wù)器,并從目標(biāo)服務(wù)器獲取數(shù)據(jù)
-
- 最后將數(shù)據(jù)返回給前端應(yīng)用。
-
這樣通過(guò)后端服務(wù)器作為中間層代理轉(zhuǎn)發(fā)請(qǐng)求,可以繞過(guò)瀏覽器同源策略的限制,實(shí)現(xiàn)跨域數(shù)據(jù)的獲取。
實(shí)現(xiàn)過(guò)程:
-
- 創(chuàng)建一個(gè)XMLHttpRequest對(duì)象并發(fā)送一個(gè)GET請(qǐng)求到后端(
http://192.168.1.63:3000
)。onreadystatechange
事件處理器會(huì)在請(qǐng)求狀態(tài)改變時(shí)觸發(fā),并在請(qǐng)求完成且響應(yīng)狀態(tài)碼為200 OK
時(shí)打印出響應(yīng)文本。 -
<script>
const xhr = new XMLHttpRequest();
xhr.open('GET', 'http://192.168.1.63:3000');
xhr.send();
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
</script>
-
- 后端創(chuàng)建一個(gè)簡(jiǎn)單的HTTP服務(wù)器,監(jiān)聽(tīng)3000端口,設(shè)置響應(yīng)頭
Access-Control-Allow-Origin
為*
,允許任何來(lái)源都可以訪問(wèn)此資源,解決跨域問(wèn)題。 -
- 再創(chuàng)建一個(gè)新的HTTP請(qǐng)求到目標(biāo)服務(wù)器
192.168.1.63:3000
,與前端設(shè)置的要一致,并將從目標(biāo)服務(wù)器收到的數(shù)據(jù)轉(zhuǎn)發(fā)回原始請(qǐng)求者。 -
const http = require('http');
http.createServer(function(req, res) {
res.writeHead(200, {
"access-control-allow-origin": "*"
});
const options = {
host: '192.168.1.63',
port: '3000',
path: '/',
method: 'GET',
headers: {}
};
http.request(options, proxyRes => {
proxyRes.on('data', function(data) {
res.end(data.toString())
});
}).end();
}).listen(3000);
請(qǐng)注意,這樣的代理服務(wù)器僅適用于開發(fā)環(huán)境,在生產(chǎn)環(huán)境中應(yīng)當(dāng)謹(jǐn)慎使用,因?yàn)樗赡軒?lái)安全風(fēng)險(xiǎn),如中間人攻擊等
四:nginx實(shí)現(xiàn)同源
相當(dāng)于node代理,大致原理如下:

五:Websocket實(shí)現(xiàn)同源
-
- websocket是http協(xié)議的一部分,所以它有同源策略
-
- websocket是長(zhǎng)連接,可以發(fā)送和接收消息
-
- websocket是html5新增的協(xié)議,它是一種雙向通信協(xié)議,建立在tcp之上
-
實(shí)現(xiàn)過(guò)程:
前端
-
- 創(chuàng)建一個(gè)新的
WebSocket
實(shí)例,并傳入 url
參數(shù)。 -
- 設(shè)置
onopen
事件處理器,當(dāng) WebSocket 連接成功打開時(shí),將 params
對(duì)象轉(zhuǎn)換為 JSON 字符串并通過(guò) WebSocket 發(fā)送。 -
- 設(shè)置
onmessage
事件處理器,當(dāng)從 WebSocket 接收到消息時(shí),解析接收到的數(shù)據(jù),并調(diào)用 resolve
方法,將解析后的數(shù)據(jù)作為 Promise
的結(jié)果返回。 -
- 調(diào)用
myWebSocket
函數(shù),傳入 WebSocket 服務(wù)器的 URL 和一個(gè)包含對(duì)象并使用 .then
方法處理 Promise
的解決情況 -
<script>
function myWebSocket(url, params = {}) {
return new Promise(function(resolve, reject) {
const socket = new WebSocket(url)
socket.onopen = () => {
socket.send(JSON.stringify(params))
}
socket.onmessage = function(e) {
resolve(e.data);
}
})
}
myWebSocket('ws://localhost:3000', {age: 18}).then(res => {
console.log(res);
})
</script>
后端
-
npm init -y
初始化為后端項(xiàng)目-
npm ws
安裝ws
-
- 使用
ws
庫(kù)來(lái)創(chuàng)建一個(gè) WebSocket 服務(wù)器,并監(jiān)聽(tīng)3000端口。 -
- 監(jiān)聽(tīng)
'connection'
事件,每當(dāng)有一個(gè)新的客戶端連接到 WebSocket 服務(wù)器時(shí),就會(huì)觸發(fā)此事件處理器。 -
- 為每個(gè)連接注冊(cè)一個(gè)
'message'
事件處理器,當(dāng)從客戶端接收到消息時(shí)觸發(fā)。并設(shè)置一個(gè)定時(shí)器,每?jī)擅胝{(diào)用一次 -
const WebSocket = require('ws');
const ws = new WebSocket.Server({ port: 3000 });
ws.on('connection', function(obj) {
obj.on('message', function(data) {
obj.send('歡迎訪問(wèn)');
setInterval(() => {
obj.send();
}, 2000);
});
});
六:postMessage
當(dāng)頁(yè)面一通過(guò)iframe
嵌套了頁(yè)面二,這兩個(gè)頁(yè)面因?yàn)榭缬驘o(wú)法進(jìn)行通訊,可以使用postMessage
實(shí)現(xiàn)跨域通訊
postMessage
是一種在不同窗口、文檔或框架之間安全地進(jìn)行消息傳遞的方式,它支持跨源消息傳遞
下面帶友友們實(shí)操一下:
主頁(yè) (index.html
) :
-
- 初始化
obj
對(duì)象,包含姓名和年齡信息。 -
- 當(dāng)
<iframe>
加載完成后,通過(guò) postMessage
發(fā)送 obj
對(duì)象給 detail.html
。 -
index.html
的 onmessage
事件處理器接收到 detail.html
發(fā)送的回復(fù)消息。-
<body>
<h2>首頁(yè)</h2>
<iframe id="frame" src="http://127.0.0.1:5500/postMessage/detail.html" width="800" height="500" frameborder="0"></iframe>
<script>
let obj = {name: 'midsummer', age: 18};
document.getElementById('frame').onload = function() {
this.contentWindow.postMessage(obj, 'http://127.0.0.1:8080');
window.onmessage = function(e) {
console.log(e.data);
};
};
</script>
</body>
詳情頁(yè) (detail.html
) :
-
- 接收到來(lái)自
index.html
的消息。 -
- 解析消息數(shù)據(jù),并更新頁(yè)面上的顯示內(nèi)容。
-
- 向
index.html
發(fā)送回復(fù)消息。 -
<body>
<h3>詳情頁(yè) -- <span id="title"></span> </h3>
<script>
let title = document.getElementById('title');
window.onmessage = function (e) {
let { data: {name, age}, origin } = e;
title.innerText = `${name} ${age}`;
e.source.postMessage(`midsummer現(xiàn)在${++age}歲`, origin);
};
</script>
</body>
簡(jiǎn)單來(lái)說(shuō)就是通過(guò)設(shè)置 postMessage
的第二個(gè)參數(shù)為目標(biāo)源地址,可以限制消息只能發(fā)送給指定源的窗口。當(dāng)一個(gè)窗口接收到消息時(shí),它可以通過(guò) onmessage
事件處理器來(lái)處理消息,并且可以使用 e.source
來(lái)回發(fā)消息給發(fā)送方。
七:document.domain
當(dāng)兩個(gè)頁(yè)面通過(guò)iframe
進(jìn)行嵌套,且兩個(gè)頁(yè)面的二級(jí)域名一致,可以使用document.domain
實(shí)現(xiàn)同源
,不知道二級(jí)域名的友友們可以參考下圖

主頁(yè):
<body>
<h2>首頁(yè)</h2>
<iframe id="frame" src="http://127.0.0.1:5500/postMessage/detail.html" width="800" height="500" frameborder="0"></iframe>
<script>
document.domain = '127.0.0.1';
document.getElementById('frame').onload = function() {
console.log(this.contentWindow.data);
};
</script>
</body>
詳情頁(yè):
<script>
document.domain = '127.0.0.1'
var data = 'domain'
</script>
通過(guò)設(shè)置 document.domain
放寬限制,允許在同一個(gè)頂級(jí)域名下的不同子域名之間進(jìn)行通信。在 index.html
和 detail.html
中都設(shè)置了 document.domain
為 '127.0.0.1'
,確保 index.html
和 detail.html
之間的 document.domain
是相同的,從而繞過(guò)了同源策略的限制。