1. 程式人生 > >全解跨域請求處理辦法

全解跨域請求處理辦法

為什麼會有跨域問題

我們試想一下以下幾種情況:

  1. 我們打開了一個天貓並且登入了自己的賬號,這時我們再開啟一個天貓的商品,我們不需要再進行一次登入就可以直接購買商品,因為這兩個網頁是同源的,可以共享登入相關的 cookie 或 localStorage 資料;
  2. 如果你正在用支付寶或者網銀,同時打開了一個不知名的網頁,如果這個網頁可以訪問你支付寶或者網銀頁面的資訊,就會產生嚴重的安全的問題。如果該未知網站是黑客的工具,那他就可以藉此發起 CSRF 攻擊了。顯然瀏覽器不允許這樣的事情發生;
  3. 想必你也有過同時登陸好幾個 qq 賬號的情況,如果同時開啟各自的 qq 空間瀏覽器會有一個小號模式,也就是另外再開啟一個視窗專門用來開啟第二個 qq 賬號的空間。

為了解決不同域名相互訪問資料導致的不安全問題,Netscape提出的一個著名的安全策略——同源策略,它是指同一個“源頭”的資料可以自由訪問,但不同源的資料相互之間都不能訪問。

同源策略

很明顯,上述第1個和第3個例子中,不同的天貓商店和 qq 空間屬於同源,可以共享登入資訊。qq 為了區別不同的 qq 的登入資訊,重新打開了一個視窗,因為瀏覽器的不同視窗是不能共享資訊的。而第2個例子中的支付寶、網銀、不知名網站之間是非同源的,所以彼此之間無法訪問資訊,如果你執意想請求資料,會提示異常:


No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.

那麼什麼是同源的請求呢?同源請求要求被請求資源頁面和發出請求頁面滿足3個相同:

協議相同
host相同
埠相同

簡單理解一下:


/*以下兩個資料非同源,因為協議不同*/
http://www.abc123.com.cn/item/a.js
https://www.abc123.com.cn/item/a.js

/*以下兩個資料非同源,因為域名不同*/
http://www.abc123.com.cn/item/a.js
http://www.abc123.com/item/a.js

/*以下兩個資料非同源,因為主機名不同*/
http://www.abc123.com.cn/item/a.js
http://item.abc123.com.cn/item/a.js

/*以下兩個資料非同源,因為協議不同*/
http://www.abc123.com.cn/item/a.js
http://www.abc123.com.cn:8080/item/a.js

/* 以下兩個資料非同源,域名和 ip 視為不同源
* 這裡應注意,ip和域名替換一樣不是同源的
* 假設www.abc123.com.cn解析後的 ip 是 195.155.200.134
*/
http://www.abc123.com.cn/
http://195.155.200.134/

/*以下兩個資料同源*/ /* 這個是同源的*/
http://www.abc123.com.cn/source/a.html
http://www.abc123.com.cn/item/b.js

HTTP 簡單請求和非簡單請求

http 請求滿足一下條件時稱為簡單請求,否則是非簡單請求:

  1. 請求方法是 HEAD,GET,POST 之一
  2. HTTP的頭資訊不超出以下幾種欄位:

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type
  3. Content-Type 取值僅限於 application/x-www-form-urlencoded, multipart/form-data, text/plain

非簡單請求在傳送之前會發送一次 OPTION 預請求,如果在跨域操作遇到返回 405(Method Not Allowed) 錯誤,需要服務端允許 OPTION 請求。

HTTP 跨域訪問的處理辦法及適用條件

JSOP

適用條件:請求的 GET 介面需要支援 jsonp 訪問

這裡需要強調的是,jsonp 不屬於 Ajax 的部分,它只是把 url 放入 script 標籤中實現的資料傳輸,不受同源策略限制。由於一般庫也會把它和 Ajax 封裝在一起,由於其和 Ajax 根部不是一回事,所以這裡不討論。下面是一個 jsonp 的例子:


window.jsonpCallback = console.log;
var JSONP = document.createElement("script");
JSONP.src = "http://tcc.taobao.com/cc/json/mobile_tel_segment.htm?tel=13122222222&t=" + Math.random() + "&callback=jsonpCallback";;
document.body.appendChild(JSONP);

後端支援jsonp方式(Nodejs)


var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
var params = qs.parse(req.url.split('?')[1]);
var fn = params.callback;

// jsonp返回設定
res.writeHead(200, { 'Content-Type': 'text/javascript' });
res.write(fn + '(' + JSON.stringify(params) + ')');

res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

document.domain

適用條件: host 中僅伺服器不同的情況,域名本身應該相同

www.dom.comw1.dom.com 需要同源才能訪問,可以將 document.domain 設定為 dom.com 解決該問題


document.domain = 'dom.com';

例如,我想開發一個瀏覽器外掛,發現騰訊視訊頁有個 iframe 其本身的跨域的,無法獲取其 iframe 的 DOM 物件。但域名部分相同,可以通過該方法解決.

注:如果你想設定它為完全不同的域名,那肯定會報同源錯誤的,注意使用範圍!

嵌入 iframe

適用條件: host 中僅伺服器不同的情況,域名本身應該相同

有了上面的例子就不難理解這個方法了,嚴格來說這不是一個新的方法,而是上一個方法的延伸。通過設定document.domain, 使同一個域名下不同伺服器名的頁面可以訪問資料,但值得注意的是:這個資料訪問不是相互的,外部頁面可以訪問 iframe 內部的資料,但 iframe 無法不能訪問外部的資料。

location.hash

適用條件:iframe 和其宿主頁面通訊

一個完成的 url 中 # 及後面的部分為 hash, 可以通過修改這個部分完成iframe 的和宿主直接的資料傳遞,下面演示一下 iframe 頁面(B.html)像宿主(A.html)傳資料, 反之同理:


// A.html
data = ['book', 'map', 'shelf', 'knife'];
setTimeout(() => {
location.hash = window.encodeURIComponent(data.join('/'));
}, 1000);

// B.html
window.parent.onhashchange = function (e) {
var data = window.decodeURIComponent(e.newURL.split('#')[1]).split('/');
console.log(data); // ["book", "map", "shelf", "knife"]
}

*注意反向傳遞資料時應該使用 window.parent.location.hash

window.name

適用條件:宿主頁面和 iframe 之間通訊

window物件有個name屬性,該屬性有個特徵:即在 window 的生命週期內,視窗載入的所有的頁面 (iframe) 都是共享一個 window.name 的,每個頁面對 window.name 都有讀寫的許可權,window.name 是持久存在一個視窗載入過的所有頁面中的,並不會因新頁面的載入而進行重置。

這樣在 window 中編輯 window.name 就可以在 iframe 中得到,但這個過程缺乏監聽,宿主頁面(A.html)和 iframe 頁面(B.html)相互並不知道對方在什麼時候修改該值:


// A.html
setTimeout(() => {
window.parent.name = "what!";
}, 2000);

// B.html
setTimeout(() => {
console.log(window.name); // what!
}, 2500);

postMessage

適用條件:postMessage 是 H5 提出的一個訊息互通的機制,解決 iframe 不能訊息互通的問題,也可以跨 window 通訊,語法如下:

// 在 www.siteA.com 中發出訊息
// @message{any} 要傳送的資料(注意:老版本瀏覽器只支援字串型別)
// @targetOrigin{string} 規定接收資料的域,只有其指定的域才能收到訊息,如果為"*"則沒用域的限制
// transfer{any} 與 message 一同傳送並轉移所有權
window.postMessage(message, targetOrigin, [transfer]);

// 在另一個頁面接受引數
window.onmessage = console.log;

這裡暫不談論第三個引數,因為你可能一輩子也用不到它。而 targetOrigin 最好不要使用 "*",除非你想讓所有頁面都收到你的訊息。

一種你會用到的場景(iframe):


<!-- www.siteA.com/index.html -->
<script>
window.addEventListener('message', function(e){
console.log('Get message: "' + e.data.title + '" from ' + e.origin); // 'Get message: "Saying hello to siteA!" from http://www.siteB.com'
});
</script>
<iframe src="http://www.siteB.com"></iframe>


<!-- www.siteB.com/index.html -->
<script>
function sendMessage(){
window.postMessage({title: 'Saying hello to siteA!'}, 'http://www.siteA.com');
}
setTimeout(sendMessage, 2000);
</script>

這一種僅僅是沒有了iframe,當你在同一個瀏覽器視窗同時開啟 www.siteA.comwww.siteB.com 兩個標籤時也可以這樣用


<!-- www.siteA.com/index.html -->
<script>
window.addEventListener('message', function(e){
console.log('Get message: "' + e.data.title + '" from ' + e.origin); // 'Get message: "Saying hello to siteA!" from http://www.siteB.com'
});
</script>


<!-- www.siteB.com/index.html -->
<script>
function sendMessage(){
window.postMessage({title: 'Saying hello to siteA!'}, 'http://www.siteA.com');
}
setTimeout(sendMessage, 2000);
</script>

反向代理伺服器

頁面需要訪問一些跨域介面,由於代理的存在,在伺服器看來請求是不跨域,所以使用各種請求。但需要注意 http 到 https 的相容問題。

比如當我在一些線上平臺開發網站後得到一個頁面 www.site-A.com, 而這個頁面需要請求我自己的資料伺服器data.site-B.com上的資料, 這樣同樣會產生跨域問題,但是www.site-A.com這個頁面是掛在第三方伺服器上的,解決這個問題可以採用代理伺服器的方法:


var express = require('express');
var request = require('request');
var app = express();

app.use('/api', function(req, res) {
var url = 'http://data.site-B.com/api2' + req.url;
req.pipe(request(url)).pipe(res);
});
app.use('/', function(req, res) {
var url = 'http://data.site-C.com';
req.pipe(request(url)).pipe(res);
});

當然還需要同時配置一個 host:


127.0.0.1 local.www.site-B.com

然後訪問 local.www.site-B.com 就 OK 了。

CORS

適用條件:CORS 需要服務端支援,且存在一定的相容性問題(如今你已經可以不考慮,但必要時不要忘了這個'bug')。其通過新增 http 頭關鍵字實現跨域可訪問,包括如下頭內容:

# www.siteA.com/api 返回相應需要具有如下 http 頭欄位

Access-Control-Allow-Origin: 'http://www.siteB.com' # 指定域可以請求,萬用字元'*'(必須)
Access-Control-Allow-Methods: 'GET,PUT,POST,DELETE' # 指定允許的跨域請求方式(必須)
Access-Control-Allow-Headers: 'Content-Type' # 請求中必須包含的 http 頭欄位
Access-Control-Allow-Credentials: true # 配合請求中的 withCredentials 頭進行請求驗證

通過 express 實現也很簡單,在註冊路由之前新增:


var cors = require('cors'); // 通過 npm 安裝
app.use(cors());

當然你也可以自定義一箇中間件:


// 自定義中介軟體
var cors = function (req, res, next) {
// 自定義設定跨域需要的響應頭。
res.header('Access-Control-Allow-Origin', 'http://www.siteB.com');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
next();
};

app.use(cors); // 運用跨域的中介軟體

WebSocket 協議跨域

ws 協議是 H5 中的 web 全雙工通訊解決方案,常規 http 屬於請求相應的過程,在客戶端沒有請求的情況下,服務端無法給客戶端主動推送資料,ws 協議解決了這個問題,但處於安全考慮,其同樣有同源策略的限制。

*這裡不討論通過長連線和服務端掛起請求等方法推送資料,本文只討論跨域。

下面舉個例子(依賴socket.io.js):


// 前端部分
socket.on('connect', function() {
// 監聽服務端訊息
socket.on('message', function(msg) {
console.log('data from server: ' + msg);
});

// 監聽服務端關閉
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});

document.getElementById('input').onkeyup = function(e) {
if(!e.shiftKey && !e.ctrlKey && !e.altKey && e.keyCode === 13)
socket.send(this.value);
};

// 後端部分(node.js)
var http = require('http');
var socket = require('socket.io');

// 啟http服務
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...');

// 監聽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.');
});
});

HTML 標籤中的 crossorigin 屬性

HTML 中 <img>, <video><script> 具有 crossorigin 屬性。新增屬性會使相應新增 CORS 相關 http 頭(需要伺服器支援)。同時,其還有以下可能的取值:

  • user-credentials 該請求通過 cookie 交換 user-credentials,伺服器相應需新增 Access-Control-Allow-Origin
  • anonymous 該請求不會通過 cookie 交換 user-credentials,伺服器相應需新增 Access-Control-Allow-Credentials

當只寫了 crossorigin 屬性沒有指定值時,其預設值為 "anonymous"。即以下兩行程式碼等價:


&lt;scirpt src="a.com/vendor.js" corssorigin&gt;&lt;/script&gt;
&lt;scirpt src="a.com/vendor.js" corssorigin="anonymous"&gt;&lt;/script&gt;

幾種不同的跨域方法比較

方法 使用條件 使用條件是否與後端互動 優點 缺點
JSONP 服務端支援 jsonp 請求 相容所有瀏覽器 只支援 GET 請求,只能和服務端通訊
CORS 伺服器相應需要相關投資端支援 方便的錯誤處理,支援所有http請求型別 存在瀏覽器相容性問題(如今可以忽略了)
document.domain 僅需要跨子域發起請求 使用便捷,沒有相容問題 對於完全不同的域名無法使用
postMessage 瀏覽器不同 window 間通訊、 iframe 和其宿主通訊 支援瀏覽器頁面間或頁面和 iframe 間同行 需要瀏覽器相容 H5 介面
window.name iframe 和其宿主通訊 簡單易操作 資料暴露在全域性不安全
location.hash iframe 和其宿主通訊 簡單易操作 資料在 url 中不安全並且有長度限制
反向代理 - 任何情況都可用 使用比較麻煩,需要自己建立服務

擴充套件:基於 webpack 的反向代理配置示例

新增 webpack 配置如下:


const config = {
// ...
devServer: {
// ...
proxy: {
'/api': {
target: 'https://data.site-B.com/api2',
changeOrigin: true, // 允許跨域
secure: false // 允許訪問 https
},
'/': {
target: 'https://data.site-C.com',
changeOrigin: true,
secure: false
},
}
}
};
module.exports = config;

擴充套件:基於 Nginx 反向代理和CORS配置示例

  • CORS 配置

location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods: GET,PUT,POST,DELETE;
}
  • 反向代理配置

server {
listen 7001;
server_name www.domain1.com;

location / {
proxy_pass http://www.B.com:7001; #反向代理
}
}

相關文章
前端常見跨域解決方案(全)
用本地執行的demo快速入門跨域

來源:https://segmentfault.com/a/1190000016590427