跨域問題的處理
什麼是跨域?
同源政策控制了不同源之間的互動,例如在使用XMLHttpRequest或标簽時則會受到同源政策的限制。這些互動通常分為三類:
- 通常允許跨域寫操作(Cross-origin writes)。例如連結(links),重定向以及表單送出。
- 通常允許跨域資源嵌入(Cross-origin embedding)。
- 通常不允許跨域讀操作(Cross-origin reads)。但常可以通過内嵌資源來巧妙的進行讀取通路。
下面為允許跨域資源嵌入的示例,即一些不受同源政策影響的标簽示例:
- <script src="…"></script>标簽嵌入跨域腳本。文法錯誤資訊隻能在同源腳本中捕捉到。
- <link rel=“stylesheet” href="…" target="_blank" rel="external nofollow" >标簽嵌入CSS。
- <img>嵌入圖檔。支援的圖檔格式包括PNG,JPEG,GIF,BMP,SVG。
- <video> 和 <audio>嵌入多媒體資源。
- <frame>和<iframe>載入的任何資源。
同源政策
同源政策限制了從同一個源加載的文檔或腳本如何與來自另一個源的資源進行互動。這是一個用于隔離潛在惡意檔案的重要安全機制。浏覽器的同源政策的目的就是為了防止 XSS,CSRF 等惡意攻擊。
隻有資源之間的協定,域名和端口号都相同,才是同一個源。
跨域解決方案
1. JSONP
由于浏覽器同源政策是允許 script 标簽這樣的跨域資源嵌套的,是以 script 标簽的資源不受同源政策的限制。JSONP 的解決方案就是通過 script 标簽進行跨域請求,由其 src 屬性發送請求到伺服器,伺服器傳回 JavaScript 代碼,浏覽器接受響應,然後就直接執行了,這和通過 script 标簽引用外部檔案的原理是一樣的。
JSONP由兩部分組成:
回調函數
和
資料
,回調函數一般是在浏覽器控制,作為參數發往伺服器端當伺服器響應時,伺服器端就會把該函數和資料拼成字元串傳回。
JSONP的請求過程:
- 請求階段:浏覽器建立一個 script 标簽,并給其src 指派(類似 http://example.com/api/?callback=jsonpCallback )。
- 發送請求:當給script的src指派時,浏覽器就會發起一個請求。
- 資料響應:服務端将要傳回的資料作為參數和函數名稱拼接在一起(格式類似”jsonpCallback({name: ‘abc’})”)傳回。當浏覽器接收到了響應資料,由于發起請求的是 script,是以相當于直接調用 jsonpCallback 方法,并且傳入了一個參數。
function jsonp({url, param, cb}){
return new Promise((resolve, reject)=>{
let script = document.createElement('script')
window[cb] = function(data){
resolve(data);
document.body.removeChild(script)
}
params = {...params, cb}
let arrs = [];
for(let key in params){
arrs.push(`${key}=${params[key]}`)
}
script.src = `${url}?${arrs.join('&')}`
document.body.appendChild(script)
})
}
jsonp({
url: 'http://localhost:3000/say',
params: {wd: 'haoxl'},
cb: 'show'
}).then(data=>{
console.log(data)
})
優缺點:
- 相容性好,低版本的 IE 也支援這種方式。
- 隻能支援 GET 方式的 HTTP 請求。
2. CORS
CORS 跨域資源共享允許在服務端進行相關設定後,可以進行跨域通信。
服務端未設定 CORS 跨域字段,服務端會拒絕請求并提示錯誤警告。
服務端設定 Access-Control-Allow-Origin 字段,值可以是具體的域名或者 ‘*’ 通配符,配置好後就可以允許跨域請求資料。
3.location.hash + iframe
location.hash + iframe 跨域通信的實作是這樣的:
- 不同域的 a 頁面與 b 頁面進行通信,在 a 頁面中通過 iframe 嵌入 b 頁面,并給 iframe 的 src 添加一個 hash 值。
- b 頁面接收到了 hash 值後,确定 a 頁面在嘗試着與自己通信,然後通過修改 parent.location.hash 的值,将要通信的資料傳遞給 a 頁面的 hash 值。
- 但由于在 IE 和 Chrmoe 下不允許子頁面直接修改父頁面的 hash 值,是以需要一個代理頁面,通過與 a 頁面同域的 c 頁面來傳遞資料。
- 同樣的在 b 頁面中通過 iframe 嵌入 c 頁面,将要傳遞的資料通過 iframe 的 src 連結的 hash 值傳遞給 c 頁面,由于 a 頁面與 c 頁面同域,c 頁面可以直接修改 a 頁面的 hash 值或者調用 a 頁面中的全局函數。
大緻流程:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuATN5UTNzIjM5ITMxgTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
a 頁面:
<script>
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = "http://localhost:8081/b.html#data";
document.body.appendChild(iframe);
function checkHash() {
try {
var data = location.hash ? location.hash.substring(1) : '';
console.log('獲得到的資料是:', data);
}catch(e) {}
}
window.addEventListener('hashchange', function(e) {
console.log('監聽到hash的變化:', location.hash.substring(1));
})
</script>
b頁面:
<script>
switch(location.hash) {
case '#data':
callback();
break;
}
function callback() {
var data = "testHash"
try {
parent.location.hash = data;
}catch(e) {
var ifrproxy = document.createElement('iframe');
ifrproxy.style.display = 'none';
ifrproxy.src = 'http://localhost:8080/c.html#' + data;
document.body.appendChild(ifrproxy);
}
}
</script>
c頁面:
<script>
// 修改 a 頁面的 hash 值
parent.parent.location.hash = self.location.hash.substring(1);
// 調用 a 頁面的全局函數
parent.parent.checkHash();
</script>
優缺點:
- hash 傳遞的資料容量有限。
- 資料直接暴露在 url 中。
4.window.name + iframe
window.name 指的是目前浏覽器視窗的名稱,預設為空字元串,每個視窗的 window.name 都是獨立的。iframe 嵌套的頁面中也有屬于自己的 window 對象,這個 window 是top window 的子視窗,也同樣擁有 window.name 的屬性。
window.name 的獨特之處在于當在頁面設定 window.name 的值,其實就是相當于給這個視窗設定了名稱,而後在這個視窗加載其他頁面(甚至不同域的頁面),window.name 的值依然存在(如果沒有重新設定那麼值不會變化),并且 window.name 的值支援比較大的存儲(2MB)。
例如: 随便找個頁面打開控制台,給目前視窗設定名稱。
設定好之後可以在這個視窗下跳轉到其他頁面。
頁面跳轉到了百度首頁,但是 window.name 的值依然是之前設定的值,因為是在一個視窗中跳轉的頁面,視窗名稱并不會被修改。具體的跨域解決方式如下:
http://localhost:8080/a.html 與 http://localhost:8081/b.html 跨域通信,a 頁面通過 iframe 嵌套 b 頁面,b 頁面中設定好 window.name 的值,由于是不同域,a 頁面不能直接通路到 b 頁面設定的 window.name 的值,需要一個與 a 頁面同域的中間頁來代理作為 a 頁面與 b 頁面通信的橋梁。
a頁面
<script>
var data = null;
var state = 0;
var iframe = document.createElement('iframe');
iframe.src = "http://localhost:8081/b.html";
iframe.style.display = 'none';
document.body.appendChild(iframe);
// 第一次加載先加載 b.html,b.html 設定好了 window.name 的值
// 而後加載 c.html,c.html 的 window.name 的值就是之前 b.html 設定的值
// 同域的情況下,a.html 可以通過 iframe.contentWindow.name 擷取到 b.html 中 windoa.name 的值
iframe.onload = function() {
if(state === 0) {
iframe.src = "http://localhost:8080/c.html";
state = 1;
}else if(state === 1) {
data = iframe.contentWindow.name;
console.log('收到資料:', data);
}
}
</script>
b頁面
<script>
window.name = '這是傳遞的資料';
</script>
5.window.postMessage
postMessage 方法接受兩個必要的參數:
- message: 需要傳遞的資料。
- targetOrigin: 資料傳遞的目标視窗域名,值可以是具體的域名或者 ‘*’ 通配符。
a頁面
<iframe src="http://localhost:8081/b.html" style='display: none;'></iframe>
<script>
window.onload = function() {
var targetOrigin = 'http://localhost:8081';
var data = {
name: 'lee',
};
// 向 b.html 發送消息
window.frames[0].postMessage(data, targetOrigin);
// 接收 b.html 發送的資料
window.addEventListener('message', function(e) {
console.log('b.html 發送來的消息:', e.data);
})
}
</script>
b頁面
<script>
var targetOrigin = 'http://localhost:8080';
window.addEventListener('message', function(e) {
if(e.source != window.parent) {
return;
}
// 接收 a.html 發送的資料
console.log('a.html 發送來的消息:', e.data);
// 向 a.html 發送消息
parent.postMessage('哈哈,我是b頁面,我收到你的消息了', targetOrigin);
})
</script>
6.websocket
WebSocket對象提供了用于建立和管理 WebSocket 連接配接,以及可以通過該連接配接發送和接收資料的 API。它是基于TCP的全雙工通信,即服務端和用戶端可以雙向進行通訊,并且允許跨域通訊。基本協定有ws://(非加密)和wss://(加密)
//socket.html
let socket = new WebSocket('ws://localhost:3000');
// 給伺服器發消息
socket.onopen = function() {
socket.send('hello server')
}
// 接收伺服器回複的消息
socket.onmessage = function(e) {
console.log(e.data)
}
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//npm i ws
// 設定伺服器域為3000端口
let wss = new WebSocket.Server({port:3000});
//連接配接
wss.on('connection', function(ws){
// 接收用戶端傳來的消息
ws.on('message', function(data){
console.log(data);
// 服務端回複消息
ws.send('hello client')
})
})