跨域的正確打開方式

跨域是前端開發平常工做中常常會面對的一個問題。平常工做中咱們都會使用像webpack-dev-server構建咱們的開發環境的接口代理、亦或是使用Charles等接口代理工具。上線後能夠經過運維同窗配合nginx或是cors等方案來解決。javascript

什麼是跨域?

在JavaScript中,有一個很重要的安全性限制,被稱爲「Same-Origin Policy」(同源策略)。這一策略對於JavaScript代碼可以訪問的頁面內容作了很重要的限制,即JavaScript只能訪問與包含它的文檔在同一域下的內容。跨域是指瀏覽器不能執行其餘網站的腳本。MDN上的解釋(瀏覽器的同源策略限制了從同一個源加載的文檔或腳本如何與另外一個源的資料進行交互,這是一個用於隔離潛在惡意文件的重要機制)。簡而言之就是瀏覽器對腳本實施的安全機制。html

同源的定義

有兩個頁面的協議、端口(若是有指定)和主機都相同,則兩個頁面具備相同的源,即爲同源。若協議/端口/主機 有一項不一樣,則爲說明二者非同源。前端

Url 調用 Url 結果
www.peanutyu.site/home www.peanutyu.site/api/* 調用成功,非跨域
www.peanutyu.site/home www.peanut.site/api/* 調用失敗,主域名不一樣
www.peanutyu.site/home www.peanutyu.site/api/* 調用失敗,協議不一樣
www.peanutyu.site/home blog.peanutyu.site/api/* 調用失敗,子域名不一樣
www.peanutyu.site/home www.peanutyu.site:8080/api/* 調用失敗,端口號不一樣

JSONP跨域(JSON with padding)

在HTML標籤裏,一些標籤好比script、img、iframe這些獲取資源的標籤是沒有跨域限制的,JSONP就是咱們去動態的建立一個Script標籤再去請求一個帶參網址來實現跨域通訊。因爲script標籤加載資源的方式是GET請求,因此JSONP只能發送GET請求。java

後臺接口設計

const xxService = require('../../service/xxService');
exports = module.exports = new class {
  constructor() {}
  
  jsonp () {
    let [cb, username ] = [];
    if (ctx.query) {
      ({ cb, username } = ctx.query);
    }
    const data = await xxService.xxMethods(username);
    // cb參數是先後端約定的方法名字,後端返回一個直接執行的方法給前端,前端獲取這個方法後立馬執行,而且把返回的數據放在方法的參數裏。
    ctx.body = `${cb}(${JSON.stringify(data)})`;
  }
}
複製代碼

前端方法實現

原生的實現方式

const script = document.createElement('script');
  const body = document.body;
  script.src = 'http://127.0.0.1:3000/api/jsonp?cb=callbackJsonp&username=peanut';
  body.appendChild(script);

  function callbackJsonp(res) {
    const div = document.createElement('div');
    div.innerText = JSON.stringify(res);
    body.appendChild(div);
    body.removeChild(script);
  }
複製代碼

Jquery的實現方式

$.ajax({
    url: 'http://blog.peanutyu.site/api/*',
    type: 'GET',
    dateType: 'jsonp', // 設置請求方式爲jsonp
    jsonpCallback: 'callbackJsonp',
    data: {
      'username': 'peanut',
    },
  });

  function callbackJsonp(res) {
    console.log(res);
  }
複製代碼

iframe跨域

document.domain + iframe 跨域

這種跨域方式要求主域名相同。好比www.peanut.site、blog.peanut.site、 a.peanutyu.site這三者主域名都是peanutyu.site。主域名不一樣就不能使用這種跨域方式。webpack

瀏覽器不一樣域的頁面之間是不能夠經過JS來進行交互操做的。可是不一樣的頁面,是可以獲取到彼此的window對象的。可是,咱們只能獲取到一個幾乎無用的window對象。好比一個頁面它的地址爲http://www.peanutyu.site/a.html,在這一個頁面裏有一個iframe,它的src爲http://peanutyu.site/b.html,這個頁面和它內部的iframe是不一樣域的,因此咱們是沒法經過在頁面中書寫js代碼來獲取iframe中的東西的。咱們只須要把http://www.peanutyu.site/a.html和http://peanutyu.site/b.html都設置成相同的域名便可。ios

但須要注意的是document.domain的設置是有限制的,咱們只能把document.domain設置成自身或更高一層的父域,而且主域必須相同。blog.peanutyu.site中某個文檔能夠設置document.domain爲blog.peanutyu.site或者peanutyu.site中的任何一個,可是不能設置爲a.blog.peanutyu.site。由於這是當前域的子域,也不可設置爲baidu.com,由於主域不一樣。nginx

假設咱們要在http://www.peanutyu.site/a.html的頁面裏訪問http://peanutyu.site裏面的數據web

在http://www.peanutyu.site/a.html設置document.domainajax

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>A頁面</title>
</head>
<body>
  <iframe id="iframe" src="http://peanutyu.site/b.html" style="display:none;"></iframe>
  <script> $(function () { try { document.domain = "peanutyu.site"; //這裏將document.domain設置成同樣 } catch (e) { } $("#iframe").load(function () { var iframe = $("#iframe").contentDocument.$; iframe.get("http://peanutyu.site/api", function (data) { console.log(data); }); }); }); </script>
</body>
</html>
複製代碼

在http://peanutyu.site/b.html也須要設置document.domain。json

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>B頁面</title>
</head>
<body>
  <script> $(function () { try { document.domain = "peanutyu.site"; //這裏將document.domain設置成同樣 } catch (e) { } }); </script>
</body>
</html>
複製代碼

這裏須要注意,在A頁面內須要等待加載完B頁面以後才能夠獲取到B頁面中的對象。獲取到對象咱們即可以直接發送ajax請求,不過這種跨域方式只能夠在主域相同的時候使用。

window.name + iframe 跨域

當iframe頁面跳轉到其餘地址時,其window.name值保持不變而且能夠支持存儲很是長的name(2MB)。可是瀏覽器規定瀏覽器跨域iframe禁止互相調用或者傳遞值。可是調用iframe時window.name卻不變,咱們正好可使用這個特性來互相傳值,固然跨域下是不允許讀取iframe的window.name的值。

由於規定若是index.html頁面和該頁面裏的iframe的src若是不一樣源,就沒法操做iframe內部的任何內容,因此也獲取不到iframe的window.name屬性了。不過既然要同源,咱們能夠準備一個和主頁面http://www.peanut.com/a.html相同域下的代理空頁面http://www.peanut.com/proxy.html來指定src。

假設咱們有一個頁面http://peanutyu.site/a.html須要從http://peanut.site/data.html內獲取到數據

data頁面代碼

window.name = '我是data頁面的數據';
複製代碼

a頁面代碼

const iframe = document.createElement('iframe');
iframe.style.display = 'none';
let state = 0;

iframe.onload = function() {
  if (state === 1) {
    const data = iframe.contentWindow.name;
    iframe.contentWindow.document.write('');
    iframe.contentWindow.close();
    document.body.removeChild(iframe);
  } else {
    state = 1;
    iframe.contentWindow.location = 'http://peanutyu.site/proxy.html';
  }
}
iframe.src = 'http://peanut.site/data.html';
document.body.appendChild(iframe);
複製代碼

在iframe載入的過程當中,迅速重置iframe的location等同於從新載入頁面,便會從新調用iframe的onload方法這時咱們的會走到條件爲state === 1的內部,獲取iframe的window.name的值,因爲調用iframe時window.name不變,因此咱們便取到了不一樣域內window.name的值。

跨域資源共享CORS

簡介

CORS須要瀏覽器和服務器同時支持。目前,全部瀏覽器都支持該功能,IE瀏覽器不能低於IE10。 整個CORS通訊過程,都是瀏覽器自動完成,不須要用戶參與。對於開發者來講,CORS通訊與同源的AJAX通訊沒有差異,代碼徹底同樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感受。 所以,實現CORS通訊的關鍵是服務器。只要服務器實現了CORS接口,就能夠跨源通訊。

兩種請求

瀏覽器將CORS請求分紅兩類:簡單請求(simple request)和非簡單請求(not-so-simple request)。只要同時知足如下兩大條件,就屬於簡單請求。 (1) 請求方法是下面三種方法之一:

  • HEAD
  • GET
  • POST

(2) HTTP的頭信息不超出如下幾種字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不一樣時知足上面兩個條件,就屬於非簡單請求。瀏覽器對這兩種請求的處理,是不同的。

簡單請求

基本流程

對於簡單請求,瀏覽器直接發出CORS請求。具體來講,就是在頭信息之中,增長一個Origin字段。下面是一個例子,瀏覽器發現此次跨源AJAX請求是簡單請求,就自動在頭信息之中,添加一個Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
複製代碼

上面的頭信息中,Origin字段用來講明,本次請求來自哪一個源(協議 + 域名 + 端口)。服務器根據這個值,決定是否贊成此次請求。

若是Origin指定的源,不在許可範圍內,服務器會返回一個正常的HTTP迴應。瀏覽器發現,這個迴應的頭信息沒有包含Access-Control-Allow-Origin字段(詳見下文),就知道出錯了,從而拋出一個錯誤,被XMLHttpRequest的onerror回調函數捕獲。注意,這種錯誤沒法經過狀態碼識別,由於HTTP迴應的狀態碼有多是200。

若是Origin指定的域名在許可範圍內,服務器返回的響應,會多出幾個頭信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
複製代碼

上面的頭信息之中,有三個與CORS請求相關的字段,都以Access-Control-開頭。

Access-Control-Allow-Origin

該字段是必須的。它的值要麼是請求時Origin字段的值,要麼是一個*,表示接受任意域名的請求。

Access-Control-Allow-Credentials

該字段可選。它的值是一個布爾值,表示是否容許發送Cookie。默認狀況下,Cookie不包括在CORS請求之中。設爲true,即表示服務器明確許可,Cookie能夠包含在請求中,一塊兒發給服務器。這個值也只能設爲true,若是服務器不要瀏覽器發送Cookie,刪除該字段便可。

Access-Control-Expose-Headers

該字段可選。CORS請求時,XMLHttpRequest對象的getResponseHeader()方法只能拿到6個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其餘字段,就必須在Access-Control-Expose-Headers裏面指定。上面的例子指定,getResponseHeader('FooBar')能夠返回FooBar字段的值。

withCredentials 屬性

上面說到,CORS請求默認不發送Cookie和HTTP認證信息。若是要把Cookie發到服務器,一方面要服務器贊成,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials: true
複製代碼

另外一方面,開發者必須在AJAX請求中打開withCredentials屬性。

var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
複製代碼

不然,即便服務器贊成發送Cookie,瀏覽器也不會發送。或者,服務器要求設置Cookie,瀏覽器也不會處理。可是,若是省略withCredentials設置,有的瀏覽器仍是會一塊兒發送Cookie。這時,能夠顯式關閉withCredentials。

xhr.withCredentials = false;
複製代碼

須要注意的是,若是要發送Cookie,Access-Control-Allow-Origin就不能設爲星號,必須指定明確的、與請求網頁一致的域名。同時,Cookie依然遵循同源政策,只有用服務器域名設置的Cookie纔會上傳,其餘域名的Cookie並不會上傳,且(跨源)原網頁代碼中的document.cookie也沒法讀取服務器域名下的Cookie。

非簡單請求

預檢請求

非簡單請求是那種對服務器有特殊要求的請求,好比請求方法是PUT或DELETE,或者Content-Type字段的類型是application/json。 非簡單請求的CORS請求,會在正式通訊以前,增長一次HTTP查詢請求,稱爲"預檢"請求(preflight)。 瀏覽器先詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些HTTP動詞和頭信息字段。只有獲得確定答覆,瀏覽器纔會發出正式的XMLHttpRequest請求,不然就報錯。 下面是一段瀏覽器的JavaScript腳本。

var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
複製代碼

上面代碼中,HTTP請求的方法是PUT,而且發送一個自定義頭信息X-Custom-Header。 瀏覽器發現,這是一個非簡單請求,就自動發出一個"預檢"請求,要求服務器確承認以這樣請求。下面是這個"預檢"請求的HTTP頭信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
複製代碼

"預檢"請求用的請求方法是OPTIONS,表示這個請求是用來詢問的。頭信息裏面,關鍵字段是Origin,表示請求來自哪一個源。除了Origin字段,"預檢"請求的頭信息包括兩個特殊字段。

Access-Control-Request-Method

該字段是必須的,用來列出瀏覽器的CORS請求會用到哪些HTTP方法,上例是PUT。

Access-Control-Request-Headers

該字段是一個逗號分隔的字符串,指定瀏覽器CORS請求會額外發送的頭信息字段,上例是X-Custom-Header。

預檢請求的迴應

服務器收到"預檢"請求之後,檢查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段之後,確認容許跨源請求,就能夠作出迴應。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
複製代碼

上面的HTTP迴應中,關鍵的是Access-Control-Allow-Origin字段,表示http://api.bob.com能夠請求數據。該字段也能夠設爲星號,表示贊成任意跨源請求。

Access-Control-Allow-Origin: *
複製代碼

若是瀏覽器否認了"預檢"請求,會返回一個正常的HTTP迴應,可是沒有任何CORS相關的頭信息字段。這時,瀏覽器就會認定,服務器不一樣意預檢請求,所以觸發一個錯誤,被XMLHttpRequest對象的onerror回調函數捕獲。控制檯會打印出以下的報錯信息。

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
複製代碼

服務器迴應的其餘CORS相關字段以下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
複製代碼
Access-Control-Allow-Methods

該字段必需,它的值是逗號分隔的一個字符串,代表服務器支持的全部跨域請求的方法。注意,返回的是全部支持的方法,而不單是瀏覽器請求的那個方法。這是爲了不屢次"預檢"請求。

Access-Control-Allow-Headers

若是瀏覽器請求包括Access-Control-Request-Headers字段,則Access-Control-Allow-Headers字段是必需的。它也是一個逗號分隔的字符串,代表服務器支持的全部頭信息字段,不限於瀏覽器在"預檢"中請求的字段。

Access-Control-Allow-Credentials

該字段與簡單請求時的含義相同。

Access-Control-Max-Age

該字段可選,用來指定本次預檢請求的有效期,單位爲秒。上面結果中,有效期是20天(1728000秒),即容許緩存該條迴應1728000秒(即20天),在此期間,不用發出另外一條預檢請求。

瀏覽器的正常請求和迴應

一旦服務器經過了"預檢"請求,之後每次瀏覽器正常的CORS請求,就都跟簡單請求同樣,會有一個Origin頭信息字段。服務器的迴應,也都會有一個Access-Control-Allow-Origin頭信息字段。

下面是"預檢"請求以後,瀏覽器的正常CORS請求。

PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
複製代碼

上面頭信息的Origin字段是瀏覽器自動添加的。下面是服務器正常的迴應。

Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8
複製代碼

上面頭信息中,Access-Control-Allow-Origin字段是每次迴應都一定包含的。

與JSONP的比較

CORS與JSONP的使用目的相同,可是比JSONP更強大。 JSONP只支持GET請求,CORS支持全部類型的HTTP請求。JSONP的優點在於支持老式瀏覽器,以及能夠向不支持CORS的網站請求數據。

WebSocket協議跨域

WebSocket 是 HTML5 開始提供的一種在單個 TCP 鏈接上進行全雙工通信的協議。

WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,容許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器只須要完成一次握手,二者之間就直接能夠建立持久性的鏈接,並進行雙向數據傳輸。

原生WebSocket API使用起來不太方便,咱們使用Socket.io,它很好地封裝了webSocket接口,提供了更簡單、靈活的接口,也對不支持webSocket的瀏覽器提供了向下兼容。

前端代碼

<div>
  <input type="text" id="inputText">
</div>
<script src="example.com/socket.io.js"></script>
<script> var socket = io('http://www.peanutyu.site'); // 鏈接成功處理 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('inputText').onblur = function() { socket.send(this.value); }; </script>
複製代碼

Node Server

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.'); 
    });
});
複製代碼

HTML5 postMessage

HTML5 window.postMessage是一個安全的、基於事件的消息API。

在須要發送消息的源窗口調用postMessage方法就能夠向外發送消息。其中源窗口能夠是如下的幾種狀況。

  1. 全局的window對象 (var win = window)
  2. 文檔窗口中的iframe(var win = iframe.documentWindow)
  3. 當前文檔窗口的父窗口(var win = window.parent)
  4. JavaScript打開的新窗口(var win = window.open())

發送消息

win.postMessage(msg, targetOrigin);
複製代碼

postMessage接受兩個參數

  1. msg, 須要發送的消息,能夠是一切JavaScript參數;如字符串、數字、對象、數組等。
  2. targetOrigin, 這個參數爲須要消息的目標域,假設www.peanutyu.site的網頁上須要往www.peanut.site的網頁上傳遞消息,那麼這個參數就是http://www.peanut.site/。若是目標窗口的協議、主機地址或端口這三者的任意一項不匹配targetOrigin提供的值,那麼消息就不會被髮送;只有三者徹底匹配,消息纔會被髮送。該值也能夠傳入一個字符串'*'表示無限制)

接收消息

window.addEventListener('message', function receiveMessage(event) {
  if (event.origin === 'http://www.peanut.site') {
    console.log(event.data); // 傳遞的數據
  }
}, false);
複製代碼

event的屬性有

  1. data 從其餘 window 中傳遞過來的對象。
  2. origin 調用 postMessage 時消息發送方窗口的 origin . 這個字符串由 協議、「://「、域名、「 : 端口號」拼接而成。例如 「example.org (隱含端口 443)」、「example.net (隱含端口 80)」、「example.com:8080」。請注意,這個origin不能保證是該窗口的當前或將來origin,由於postMessage被調用後可能被導航到不一樣的位置。
  3. source 對發送消息的窗口對象的引用; 您可使用此來在具備不一樣origin的兩個窗口之間創建雙向通訊。

Nginx代理

Nginx配置

server{
    # 監聽9999端口
    listen 9999;
    # 域名是localhost
    server_name localhost;
    #凡是localhost:9999/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871 
    location ^~ /api {
        proxy_pass http://localhost:9871;
    }    
}
複製代碼

請求的時候直接用回前端這邊的域名http://localhost:9999,這就不會跨域,而後Nginx監聽到凡是localhost:9099/api這個樣子的,都轉發到真正的服務端地址http://localhost:9871

axios.get('http://localhost:9999/api/iframePost', params).then(result => console.log(result)).catch(() => {});
複製代碼

Nginx轉發的方式彷佛很方便!但這種使用也是看場景的,若是後端接口是一個公共的API,好比一些公共服務獲取天氣什麼的,前端調用的時候總不能讓運維去配置一下Nginx,若是兼容性沒問題(IE 10或者以上),CROS纔是更通用的作法吧。

Node中間層接口轉發

目前中後臺比較經常使用的接口處理方式。Spa頁面經過服務器根路由或者/index渲染由前端來控制路由跳轉。剩下/api路徑下開發咱們的接口請求。

後臺配置

// 頁面路由
router.get('/index', async function(ctx, next) {
  // 打包JS時間戳
  let timeT = moment().valueOf();
  // 配置基本版本號
  let buildPath = config.assetsServerName;
  try {
    let env = process.env.NODE_ENV;
    // 從Redis內獲取的JS版本號
    let configInfo = await RedisService.getServerConfigInfoByEnv(env);
    if(configInfo) {
      let info = JSON.parse(configInfo);
      if(info && info['build']) {
        buildPath = info['build'].url;
      }
    }
    // 渲染SPA頁面
    await ctx.render('index', {assetsPath: buildPath, tag: timeT});
  } catch(e) {
    // 報錯渲染配置版本號SPA頁面
    await ctx.render('index', {assetsPath: buildPath, tag: timeT});
  }
})

// 接口路由
router.get('/api/list', xxController.methods); // 獲取列表方法、 具體邏輯處理經過controller完成
複製代碼

前端代碼

_reqData() {
  axios.get('/api/list', {}).then(result => {console.log(result)}).catch(() => {});
}

componentWillMount() {
  this._reqData();
}
複製代碼

參考連接

相關文章
相關標籤/搜索