非面試向跨域實踐詳解

前言

筆者常常在前端開源羣答疑,加上以前的招聘面試經歷。發現許多新手前端在問起跨域問題的解決方案,一套一套的,但是實際遇到跨域問題了就不知道怎麼解決了。此次寫這篇文章從實踐角度聊一聊跨域問題。javascript

跨域基本概念

出於瀏覽器的同源策略限制,瀏覽器會拒絕跨域請求。 這就是跨域問題的產生緣由,同源策略是用於隔離潛在惡意文件的重要安全機制。css

這句話的三個關鍵字:html

  • 同源
  • 限制
  • 瀏覽器拒絕

什麼是同源

那麼第一個問題來了,什麼算是同源。解決這個問題須要先了解一下URL的完整結構: 前端

URL結構
兩個URL的纔算同源。反而言之,三者任何一個不相同都算跨域。 例如某個頁面地址爲 www.domain.com/page1.html, 該頁面訪問如下API接口跨域關係表:

API 接口地址 是否跨域 緣由
www.domain.com/api/users/1 協議、主機、端口所有都相同
www.domain.com:80/api/users/1 端口不一樣
www.baidu.com/api/users/1 協議不一樣
api.domain.com/v1/users/1 主機不一樣

有哪些限制

  1. XmlHttpRequest(即ajax請求)和Fetch兩種接口發出的HTTP請求進行限制。
  2. 對於嵌入資源標籤scriptimglinkvideo等標籤加載資源的請求(HTTP GET請求)不作限制。

具體的限制規則還有不少,這裏只說常見和本文用得上的。vue

瀏覽器拒絕

那麼那些環境算是瀏覽器?java

  • PC端常見的 Chrome/Safari/Edge
  • 移動端的Chrome/Safari/各個App內嵌Webview 瀏覽器又是怎麼拒絕的 先來看一張圖,一個用戶點擊了一個按鈕,發出了一個AJAX GET請求。那麼常見的流程如圖:

那麼若是用戶發出的AJAX GET請求是一個跨域請求,那麼會在上圖中哪個階段被阻止? 可是第3階段,也就是說用戶發送的信息能夠到達服務端,服務器是可以接受處理並返回了。返回的瀏覽器發現這是一個跨域請求。就直接拒絕,同時把返回的信息替換爲報錯信息,返回給JavaScript程序。 對於更復雜的POST/PUT等請求, MDN CORS文檔裏面有更詳細的處理方法。這裏就不細說。react

這一點很重要,可是老是被新人忽略。因此重要的事情說三遍,webpack

  • 拒絕跨域請求是瀏覽器
  • 拒絕跨域請求是瀏覽器
  • 拒絕跨域請求是瀏覽器

反過來講,Nginx、Java/Nodejs等編程語言的HTTPClient以及手機App,他們發出的HTTP請求就徹底沒有跨域問題,由於他們不是瀏覽器,沒有實現W3C規範。ios

跨域解決方案

JSONP

在瀏覽器中假設有如下一段代碼會執行結果會是什麼樣?nginx

<script> window.callback = function (data) { console.log(data); delete window.callback; } </script>
<script> callback({ "code": 1, "data": [1,2,3] }); </script>
複製代碼

毫無疑問,確定是在控制檯輸出了一個對象信息。 記得剛纔在介紹跨域基本概念的時候說個瀏覽器不限制script標籤加載js文件。那麼把這兩者的特性相結合。第二個script標籤改成從網絡加載. 就能夠實現跨域. 例如 一個跨域APIhttp://api.domain.com/v1/users/1

  1. 在window對象上掛載一個函數callbackFun
  2. 建立一個script標籤: <script src="http://api.domain.com/v1/users/1?callback=callbackFun"></script>
  3. script就會向服務器發出 GET http://api.domain.com/v1/users/1?callback=callbackFun的請求
  4. 讓後端返回以下內容 ContentType爲application/javascript
callbackFun({/*須要的數據*/});
複製代碼
  1. 數據返回成功之後處理數據,刪除script標籤

以上步驟就是JSONP的思想。實現一個完善的JSNOP請求庫還有細節要處理,好比超時取消、回調函數防重名等。不少開源庫(jQuery, axios)都實現了JSNOP請求。想要代碼的去Github閱讀源碼,這裏就不給出代碼。

優劣勢

JSONP雖然是一種實現跨域訪問的方法,前端想要使用JSONP進行跨域訪問卻不容易。

  1. 只支持GET方法
  2. 要後端的配合 GET http://api.domain.com/v1/users/1 返回
ContentType:	application/json
複製代碼
{
    "code": 1,
    "data" : {"userid": 1}
}
複製代碼

GET http://api.domain.com/v1/users/1?callback=callbackFun 返回

ContentType:	application/javascript
複製代碼
callbackFun({
    "code": 1,
    "data" : {"userid": 1}
});
複製代碼

既然能夠和後端商量配合你改造接口,那還有更好的方案能夠解決。何須用這種方案。

JSONP有一個有點就是兼容性好,IE678統統兼容,因此通常JSNOP是後端同窗若是主動須要開放API給他人使用,同時有須要極高的兼容的一個妥協方案。通常狀況下不推薦這個方案。

JSONP 開心一刻

真實經歷。以前開發項目須要調用另外一個項目組的接口。 跨域形成接口掉不通,而後找Z君溝通, Z君說:"你用JSONP來掉接口就行了。這都不知道...." 而後我還在想大神這麼NB的麼,JSONP兼容都提早作完了。我試了JSONP。坑爹呀,你後端根本就沒兼容JSONP,我怎麼調用,呵呵... 呵呵呵呵....

請求代理

JSONP方案不推薦,那麼又須要訪問跨域接口,怎麼辦呢? 重要事情不在意再多說一遍拒絕跨域請求是瀏覽器。 那麼若是有一個非W3C標準的HttpClient幫助咱們轉發請求,不就能夠了實現跨域訪問了。

App端

一般App對於webview都有很強的控制權,能夠在Webview的JS環境中注入一些方法。 那麼移動端程序員能夠在Webview中注入一個接口,運行在裏面的js代碼能夠經過這個方法把本身的請求地址、請求參數、請求體等數據交給App Native端,讓App Native代爲收發請求。App Native不是瀏覽器,不受跨域限制。

具體實現方法能夠搜 Hybird App開發或者請教移動端開發的同窗。

Web端

Web端必然運行在瀏覽器環境中,那麼沒有App Native。還有服務器上能夠作反向代理。 所謂的反向代理,原理和App Native請求代理的原理差很少,就是咱們請求非跨域下的反向代理服務,反向代理服務會把你的請求轉發給目標服務器。 反向代理服務能夠是Nginx也能夠是java/Nodejs程序等等。這些程序也不受跨域限制,能夠接受目標服務器的請求,並返回給咱們。

React/Vue 開發階段跨域處理

React/Vue 這種SPA開發施行的徹底的先後端分離的模式,開發階段必然是須要跨域訪問接口的。 Vue開發能夠這樣配置:

// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      '/api': {
        target: '<url>',
        ws: true,
        changeOrigin: true
      },
      '/foo': {
        target: '<other_url>'
      }
    }
  }
}
複製代碼

詳情見Vue CLI文檔。React也有相似的配置,詳情見 create-react-app文檔

那麼他們具體是怎麼實現的呢? 本地起一個服務端程序提供反向代理的能力。而React/Vue本地啓動的這個服務端程序就是Webpack-dev-server。來探索Webpack-dev-server源碼,源碼中啓動server的關鍵代碼在lib/Server.js中,挑重點

/* 此處省略許多行代碼 */

// 27行 引入express 做爲服務端框架
const express = require('express');

/* 此處省略許多行代碼 */


// 31行 引入 http-proxy-middleware 提供反向代理的能力
const httpProxyMiddleware = require('http-proxy-middleware');

/* 此處省略許多行代碼 */

// 328行 獲取 proxyMiddleware 並加載到爲express的中間件Middleware
app.use((req, res, next) = > {
    if (typeof proxyConfigOrCallback === 'function') {
        const newProxyConfig = proxyConfigOrCallback();

        if (newProxyConfig !== proxyConfig) {
            proxyConfig = newProxyConfig;
            // 334行 根據 proxyConfig 獲取 處理proxy請求的中間件proxyMiddleware
            proxyMiddleware = getProxyMiddleware(proxyConfig);
        }
    }

    const bypass = typeof proxyConfig.bypass === 'function';

    const bypassUrl = (bypass && proxyConfig.bypass(req, res, proxyConfig)) || false;

    if (bypassUrl) {
        req.url = bypassUrl;

        next();
    } else if (proxyMiddleware) {
        // 347行 最最關鍵一行 通過屢次斷定某個請求是須要代理轉發的請求,那麼把它交給proxyMiddleware進行處理, proxyMiddleware
        return proxyMiddleware(req, res, next);
    } else {
        next();
    }
});
複製代碼

以上代碼有點NodeJS服務端開發的同窗基本能看明白,看不明白也不要緊。你知道React/Vue能夠經過相應的配置項得到接口跨域訪問的能力就能夠了。其中最核心的就是依靠Express的網絡請求能力充當反向代理服務器。

React/Vue 線上部署階段跨域處理

開發階段還能夠經過本地啓動一個Express服務器做爲代理,幫助咱們處理跨域問題,問題是生產環境是不推薦這麼作的。React/Vue 項目一般在build之後會生成如下文件:

  • xxx.html 文件1份
  • xxx.xxxxxx.js Javacript文件若干
  • xx.xxxx.css 文件若干
  • xxx.map 文件若干,固然也可能沒有 並且裏面的js/css/圖片等文件一般部署在cdn上,最爲要緊的頁面入口index.html則須要當心部署,不然易遇到2個問題
  1. 頁面沒辦法訪問
  2. 接口跨域致使沒辦法訪問

1

對於index.html的部署,Vue-Router文檔寫的很清楚。推薦經過nginx try-file命令來進行部署。同時nginx又是一個反向代理服務器。假設 網頁須要在host http://www.domain.com/下, 真實API服務部署在http://api.domain.com/api。那麼經過反向代理把接口代理到 http://www.domain.com/api下。那麼跨域訪問就變成了同域名訪問。 那麼nginx的配置文件能夠這樣寫

server { 
    listen       80;
    server_name  www.domain.com ;
    root www; # 存放html文件的文件夾
    location ^/api { # 接口代理到 8080
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_pass       http://api.domain.com/api;
    }
    location / { # 其餘請求返回index.html
        try_files $uri $uri/ /index.html;
    }
  }
複製代碼

這樣作完, 訪問API就會被代理轉發,訪問其餘路徑就返回html。以下圖所示:

線上部署的方式可能根據系統架構選型而多種多樣。這只是其中一種比較通用且爲官方推薦的方式。僅作參考。相似ngixn的服務端軟件仍是Caddy、Envoy

這種方案的優勢是不須要後端同窗改動接口,只須要運維小哥幫助配置一下nginx便可完成兼容。缺點是多一次轉發可能帶來性能損失。

CORS

實際狀況多種多樣,有些時候沒辦法使用JSONP,也經過nginx轉發又會產生性能損失。那麼還有一個終極大招———— CORS.

W3C的同源策略出來之後形成了不少不便,沒法應對某些跨域訪問的強需求。爲此W3C增長了CORS相關的規範, 文檔以前也說起過:MDN CORS文檔

重要的事情再重複一遍:拒絕跨域請求是瀏覽器,那麼CORS的原理就是CORS相關的規範中制定了一些響應頭(Response Header),這些響應頭以Access-Control-Request-開頭。簡單枚舉幾個,具體這些頭的含義和用法見MDN CORS文檔.

Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
複製代碼

瀏覽器在接收到設置過CORS響應頭的返回之後,會根據CORS規範檢查合法性,檢查經過則再也不阻止,放行經過。

簡而言之就是CORS響應頭就是用來告訴瀏覽器:"我是雖然是跨域請求,可是我是合法的,請不要拒絕我"。

CORS方案的優勢是支持各類方法 GET、POST、PUT、DELTE等等。並且改動量比較小。能夠在服務端程序好比Java或者NodeJS上作,也可經過前置代理服務器nginx完成。

缺點就是

  1. 瀏覽器兼容性差
  2. 下降了安全性,畢竟W3C之因此禁止跨域,是爲了安全。如今推出CORS方案雖然已經在安全和靈活方面作到一個較好平衡。可是若是CORS響應頭設置不當,仍是可能會產生安全問題。

其餘

其餘還有用與父頁面與子頁面(iframe)之間的通訊的跨域問題,window.name、postMessage等方法。這裏就不詳細說了。平常用的確實很少,有須要再查把。

要點總結

  • 跨域的基本概念
    • 跨域是W3C組織爲了保證安全指定的規範
    • 協議、主機、端口所有都相同纔是同源,不然就是跨域
    • 限制XHR與Fetch,不限制資源類標籤
    • 拒絕跨域請求是瀏覽器 拒絕跨域請求是瀏覽器 拒絕跨域請求是瀏覽器 重要事情說三遍
  • 常見跨域解決方案
    • JSONP 只能發出GET請求。通常不推薦,除非須要很強的接口兼容性
    • 訪問代理
      • APP端能夠經過Native端發請求
      • Web端能夠經過架設反向代理服務器
        • React/Vue平常開發就是經過Express服務器作的反向代理
        • 生產環節部署可使用nginx
    • CORS是W3C准許跨域規範,須要後端配合改程序
    • 其餘略過

生產環境中建議選擇順序是 反向代理 > CORS >> JSONP。 由於反向代理兼容性最好,程序改動少。 CORS適用於沒法容忍反向代理的性能損失和第三方OpenAPI訪問。 JSONP 只有當後端須要兼容性高,沒辦法部署反向代理服務器的狀況以及前端訪問第三方提供的JSONP接口。其餘任何狀況下不推薦。

相關文章
相關標籤/搜索