全解跨域請求處理辦法

為什么會有跨域問題

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

  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"。即以下兩行代碼等價:

<scirpt src="a.com/vendor.js" corssorigin></script>
<scirpt src="a.com/vendor.js" corssorigin="anonymous"></script>

幾種不同的跨域方法比較

方法 使用條件 使用條件是否與后端交互 優點 缺點
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;  #反向代理
    }
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容

  • 什么是跨域 跨域,是指瀏覽器不能執行其他網站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對JavaScript實...
    HeroXin閱讀 843評論 0 4
  • 什么是跨域 跨域,是指瀏覽器不能執行其他網站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對JavaScript實...
    Yaoxue9閱讀 1,311評論 0 6
  • 什么是跨域 跨域,是指瀏覽器不能執行其他網站的腳本。它是由瀏覽器的同源策略造成的,是瀏覽器對JavaScript實...
    他方l閱讀 1,070評論 0 2
  • 什么是跨域? 2.) 資源嵌入:、、、等dom標簽,還有樣式中background:url()、@font-fac...
    電影里的夢i閱讀 2,381評論 0 5
  • 本文是對蘋果Data Entry 文檔的簡單翻譯,如有翻譯不當之處,歡迎指正 無論是點擊界面元素還是使用鍵盤,輸入...
    Zakerberg閱讀 737評論 0 0