「深刻淺出」前端開發中經常使用的幾種跨域解決方案

感謝來自微醫大前端技術 公衆號提供的Theme模板javascript

編者薦語

本文將爲你們介紹,前端開發中,最經常使用的幾種跨域解決方案;
html

看完本文能夠系統地掌握,不一樣種跨域解決方案間的巧妙,以及它們的用法、原理、侷限性和適用的場景前端

包括如下幾個方面:java

  • 跨域的現象,和幾種常見的跨域表現
  • 跨域的解決方案(原理分析)
    • 修改本地HOST
    • JSONP
    • CORS
    • Proxy
    • Nginx反向代理
    • Post Message(利用 iframe標籤,實現不一樣域的關聯)

同源是什麼?

若是兩個URL的協議protocol、主機名host和端口號port都相同的話,則這兩個URL是同源。node

同源策略

同源策略是一個重要的安全策略。它可以阻斷惡意文檔,減小被攻擊的媒介。webpack

真實項目中,不多有同源策略,大部分都是非同源策略web

跨域是什麼?

當協議、域名與端口號中任意一個不相同時,都算做不一樣域,不一樣域之間相互請求資源的表現(非同源策略請求),稱做」跨域「ajax

跨域現象

那麼咱們就下面的網址分析一下,哪一塊是協議,哪一塊是域名及端口號express

http://kbs.sports.qq.com/index.html

協議:http(還有以一種https協議)
域名:kbs.sports.qq.com
端口號:80

https://127.0.0.1:3000

協議:https
域名:127.0.0.1
端口號:3000

假如咱們的真實項目開發中的Web服務器地址爲 」http://kbs.sports.qq.com/index.html「,而須要請求的數據接口地址爲 "http://api.sports.qq.com/list"。json

當Web服務器的地址向數據接口的地址發送請求時,便會形成了跨域現象

形成跨域的幾種常見表現

  • 服務器分開部署(Web服務器 + 數據請求服務器)
  • 本地開發(本地預覽項目 調取 測試服務器的數據)
  • 調取第三方平臺的接口

Web服務器:主要用來靜態資源文件的處理

解決方案

  • 修改本地HOST(不做介紹)
  • JSONP
  • CORS
  • Proxy
  • Nginx反向代理
  • Post Message(利用 iframe標籤,實現不一樣域的關聯)

在後面會詳細分析這四種解決方案的原理和用法配置,以及它們的優勢和侷限性

注意: 基於ajaxfetch發送請求時,若是是跨域的,則瀏覽器默認的安全策略會禁止該跨域請求

補充說明:如下全部的測試用例,均由Web:http://127.0.0.1:5500/index.htmlAPI:http://127.0.0.1:1001/list發起請求

API接口的服務器端是本身經過express創建的,下文在服務器端以app.use中間件的形式接受來自客戶端的請求並作處理。

即 在「http://127.0.0.1:1001/list」from origin「http://127.0.0.1:55」上對XMLHttpRequest的訪問已被CORS策略阻止:被請求的資源上沒有「Access- control - allow-origin」頭

在後端開啓了一個端口號爲1001的服務器以後,咱們來實踐一下

let xhr = new XMLHttpRequest;
xhr.open('get''http://127.0.0.1:1001/list');
xhr.onreadystatechange = () => {
  if (xhr.status === 200 && xhr.readyState === 4) {
    console.log(xhr.responseText);
  }
};
xhr.send(); 
跨域的常見報錯提示

這就是因爲瀏覽器默認的安全策略禁止致使的。

下面介紹一下幾種常見的解決方案。

JSONP

原理:JSONP利用script標籤不存在域的限制,且定義一個全局執行上下文中的函數func

(用來接收服務器端返回的數據信息)來接收數據,從而實現跨域請求。

弊端

  • 只容許 GET請求
  • 不安全:只要瀏覽器支持,且存在瀏覽器的全局變量裏,則誰均可以調用

圖解JSONP的原理

手動封裝JSONP

callback必須是一個全局上下文中的函數

(防止不是全局的函數,咱們須要把這個函數放在全局上,而且從服務器端接收回信息時,要瀏覽器執行該函數)

注意:

  • uniqueName變量存儲全局的回調函數(確保每次的 callback都具備惟一性)
  • 檢驗 url中是否含有"?",有的話直接拼接 callback,沒有的話補」?「
// 客戶端
function jsonp(url, callback{
  // 把傳遞的回調函數掛載到全局上
 let uniqueName = `jsonp${new Date().getTime()}`;
  // 套了一層 anonymous function
  // 目的讓 返回的callback執行且刪除建立的標籤
  window[uniqueName] = data => {
  // 從服務器獲取結果並讓瀏覽器執行callback
    document.body.removeChild(script);
    delete window[uniqueName];
    callback && callback(data);
  }
  
  // 處理URL
  url += `${url.includes('?')} ? '&' : '?}callback=${uniqueName}'`;
  
  // 發送請求
  let script = document.createElement('script');
  script.src = url;
  document.body.appendChild(script);
}

// 執行第二個參數 callback,獲取數據
jsonp('http://127.0.0.1:1001/list?userName="lsh"', (result) => {
 console.log(result);
})
// 服務器端
// Api請求數據
app.get('/list', (req, res) => {
  
  // req.query 問號後面傳遞的參數信息
  // 此時的callback 爲傳遞過來的函數名字 (uniqueName)
 let { callback } = req.query;
  
  // 準備返回的數據(字符串)
  let res = { code0data: [10,20] };
  let str = `${callback}($(JSON.stringify(res)))`;
  
  // 返回給客戶端數據
  res.send(str);
})

測試用例展現:

  • 客戶端請求的 url
  • 服務端返回的數據
    • 返回的 callback
    • 返回的數據信息 result
// 服務器請求的 url
Request URL:
 http://127.0.0.1:1001/list?userName="lsh"&callback=jsonp159876002

// 服務器返回的函數callback
 jsonp159876002({"code":0"data": [10,20]});

// 客戶端接收的數據信息
code0dataArray(2) }

當瀏覽器發現返回的是jsonp159876002({"code":0, "data": [10,20]});這個函數,會自動幫咱們執行的。

JSONP弊端

在上文中說到只要服務器端那裏設置了容許經過jsonp的形式跨域請求,咱們就能夠取回數據。

下面是在咱們封裝完jsonp方法以後,向一個容許任何源向該服務器發送請求的網址xxx

jsonp('https://matchweb.sports.qq.com/matchUnion/cateColumns?from=pc', result => {
  console.log(result);
});

CORS

上文提到,不容許跨域的根本緣由是由於Access-Control-Allow-Origin已被禁止

那麼只要讓服務器端設置容許源就能夠了

原理:解決掉瀏覽器的默認安全策略,在服務器端設置容許哪些源請求就能夠了

先來看一下下面的設置有哪些問題

// 服務器端
app.use((req, res, next) => {
 // * 容許全部源(不安全/不能攜帶資源憑證)
 res.header("Access-Control-Allow-Origin""*");
 res.header("Access-Control-Allow-Credentials"true);

 /* res.header("Access-Control-Allow-Headers", "Content-Type,....");
 res.header("Access-Control-Allow-Methods", "GET,..."); */


 // 試探請求:在CORS跨域請求中,首先瀏覽器會本身發送一個試探請求,驗證是否能夠和服務器跨域通訊,服務器返回200,則瀏覽器繼續發送真實的請求
 req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
});

// 客戶端
let xhr = new XMLHttpRequest;
xhr.open('get''http://127.0.0.1:1001/list');
xhr.setRequestHeader('Cookie''name=jason');
xhr.withCredentials = true;
xhr.onreadystatechange = () => {
  if (xhr.status === 200 && xhr.readyState === 4) {
    console.log(xhr.responseText);
  }
};
xhr.send();

當咱們一旦在服務器端設置了容許任何源能夠請求以後,其實請求是不安全的,而且要求客戶端不能攜帶資源憑證(好比上文中的Cookie字段),瀏覽器端會報錯。

告訴咱們Cookie字段是不安全的也不能被設置的,若是容許源爲'*'的話也是不容許的。

假如在咱們的真實項目開發中

正確寫法✅

  • 設置單一源(安全/也能夠攜帶資源憑證/只能是單一一個源)
  • 也能夠動態設置多個源:每一次請求都會走這個中間件,咱們首先設置一個白名單,若是當前客戶端請求的源在白名單中,咱們把 Allow-Origin動態設置爲當前這個源
app.use((req, res, next) => {
  
  // 也可自定義白名單,檢驗請求的源是否在白名單裏,動態設置
  /* let safeList = [
  "http://127.0.0.1:5500",
  xxx,
  xxxxx,
 ]; */

 res.header("Access-Control-Allow-Origin""http://127.0.0.1:5500");
 res.header("Access-Control-Allow-Credentials"true); // 設置是否可攜帶資源憑證

 /* res.header("Access-Control-Allow-Headers", "Content-Type,....");
 res.header("Access-Control-Allow-Methods", "GET,..."); */


 // 試探請求:在CORS跨域請求中,首先瀏覽器會本身發送一個試探請求,驗證是否能夠和服務器跨域通訊,服務器返回200,則瀏覽器繼續發送真實的請求
 req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
});

CORS的好處

  • 原理簡單,容易配置,容許攜帶資源憑證
  • 仍能夠用 ajax做爲資源請求的方式
  • 能夠動態設置多個源,經過判斷,將 Allow-Origin設置爲當前源

CORS的侷限性

  • 只容許某一個源發起請求
  • 如多個源,還須要動態判斷

Proxy

Proxy翻譯爲「代理」,是由webpack配置的一個插件,叫"webpack-dev-server"(只能在開發環境中使用)

Proxy在webpack中的配置

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode'production',
  entry'./src/main.js',
  output: {...},
  devServer: {
    port'3000',
    compresstrue,
    opentrue,
    hottrue,
    proxy: {
      '/': {
        target'http://127.0.0.1:3001',
        changeOrigintrue
      }
    }
  },
  // 配置WEBPACK的插件
  plugins: [
    new HtmlWebpackPlugin({
      template`./public/index.html`,
      filename`index.html`
    })
  ]
};

圖解Proxy的原理

Proxy代理其實至關於由webpack-dev-server配置在本地建立了一個port=3000的服務,利用node的中間層代理(分發)解決了瀏覽器的同源策略限制。

可是它只能在開發環境下使用,由於dev-server只是一個webpack的一個插件;

若是須要在生產環境下使用,須要咱們配置Nginx反向代理服務器;

另外若是是本身實現node服務層代理:不管是開發環境仍是生產環境均可以處理(node中間層和客戶端是同源,中間層幫助咱們向服務器請求數據,再把數據返回給客戶端)

Proxy的侷限性

只能在本地開發階段使用

配置Nginx反向代理

主要做爲生產環境下跨域的解決方案。

原理:利用Node中間層的分發機制,將請求的URL轉向服務器端的地址

配置反向代理

server {
 listen80;
  server_name: 192.168.161.189;
  loaction: {
  proxy_pass_http://127.0.0.1:1001; // 請求轉向這個URL地址,服務器地址
    root html;
    index index.html;
  }
}

簡單寫了一下僞代碼,實際開發中根據需求來配。

POST MESSAGE

假設如今有兩個頁面,分別爲A頁面port=1001、B頁面port=1002,實現頁面A與頁面B的頁面通訊(跨域)

原理:

  • 把 B頁面當作A的子頁面嵌入到A頁面裏,經過 iframe.contentWindow.postMessage向B頁面傳遞某些信息
  • 在A頁面中經過 window.onmessage獲取A頁面傳遞過來的信息 ev.data(見下代碼)
  • 同理在B頁面中經過 ev.source.postMessage向A頁面傳遞信息
  • 在A頁面中經過 window.onmessage獲取B頁面傳遞的信息

主要利用內置的postMessageonmessage傳遞信息和接收信息。

A.html

// 把 B頁面當作A的子頁面嵌入到A頁面裏
<iframe id="iframe" src="http://127.0.0.1:1002/B.html" frameborder="0" style="display: none;"></iframe>

<script>
  iframe.onload = function ({
    iframe.contentWindow.postMessage('珠峯培訓''http://127.0.0.1:1002/');
  }

  //=>監聽B傳遞的信息
  window.onmessage = function (ev{
    console.log(ev.data);
  }
</script>

B.html

<script>
  //=>監聽A發送過來的信息
  window.onmessage = function (ev{
    // console.log(ev.data);

    //=>ev.source:A
    ev.source.postMessage(ev.data + '@@@''*');
  }
</script>

幾種方案的比較

1. JSONP方案須要先後端共同配置完成(利用script標籤不存在域的限制)【麻煩,老項目使用】

2. CORS原理簡單,但只能配置單一源,若是須要配置多個源,也只能從白名單中篩選出某一個符合表求的origin【偶爾使用】

服務器端須要單獨作處理,客戶端較爲簡單

3. Proxy客戶端經過dev-server,生產環境須要配置Nginx反向代理(利用Node中間層分發機制)【經常使用】

感謝你們

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點個「 在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
  2. 歡迎加我微信「 Augenstern9728」一塊兒交流學習...
  3. 關注公衆號「 前端時光屋」,持續爲你推送精選好文。

本文分享自微信公衆號 - 前端時光屋(javascriptlab)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索