全解跨域請求處理辦法

爲何會有跨域問題

咱們試想一下如下幾種狀況:javascript

  1. 咱們打開了一個天貓而且登陸了本身的帳號,這時咱們再打開一個天貓的商品,咱們不須要再進行一次登陸就能夠直接購買商品,由於這兩個網頁是同源的,能夠共享登陸相關的 cookie 或 localStorage 數據;
  2. 若是你正在用支付寶或者網銀,同時打開了一個不知名的網頁,若是這個網頁能夠訪問你支付寶或者網銀頁面的信息,就會產生嚴重的安全的問題。若是該未知網站是黑客的工具,那他就能夠藉此發起 CSRF 攻擊了。顯然瀏覽器不容許這樣的事情發生;
  3. 想必你也有過同時登錄好幾個 qq 帳號的狀況,若是同時打開各自的 qq 空間瀏覽器會有一個小號模式,也就是另外再打開一個窗口專門用來打開第二個 qq 帳號的空間。

爲了解決不一樣域名相互訪問數據致使的不安全問題,Netscape提出的一個著名的安全策略——同源策略,它是指同一個「源頭」的數據能夠自由訪問,但不一樣源的數據相互之間都不能訪問。html

同源策略

很明顯,上述第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個相同:java

協議相同
host相同
端口相同

簡單理解一下:node

/*如下兩個數據非同源,由於協議不一樣*/
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 請求知足一下條件時稱爲簡單請求,不然是非簡單請求:webpack

  1. 請求方法是 HEAD,GET,POST 之一
  2. HTTP的頭信息不超出如下幾種字段:web

    • 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 請求。shell

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

JSOP

適用條件:請求的 GET 接口須要支持 jsonp 訪問

這裏須要強調的是,jsonp 不屬於 Ajax 的部分,它只是把 url 放入 script 標籤中實現的數據傳輸,不受同源策略限制。因爲通常庫也會把它和 Ajax 封裝在一塊兒,因爲其和 Ajax 根部不是一回事,因此這裏不討論。下面是一個 jsonp 的例子:express

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)npm

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;  #反向代理
    }
}
相關文章
相關標籤/搜索