前言
筆者經常在前端開源群答疑,加上之前的招聘面試經歷。發現許多新手前端在問起跨域問題的解決方案,一套一套的,可是實際遇到跨域問題了就不知道怎麼解決了。這次寫這篇文章從實踐角度聊一聊跨域問題。
跨域基本概念
出於瀏覽器的同源策略限制,瀏覽器會拒絕跨域請求。 這就是跨域問題的產生原因,同源策略是用於隔離潛在惡意檔案的重要安全機制。
這句話的三個關鍵字:
- 同源
- 限制
- 瀏覽器拒絕
什麼是同源
那麼第一個問題來了,什麼算是同源。解決這個問題需要先了解一下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 | 是 | 主機不同 |
有哪些限制
- XmlHttpRequest(即ajax請求)和Fetch兩種介面發出的HTTP請求進行限制。
- 對於嵌入資源標籤
script
、img
、link
、video
等標籤載入資源的請求(HTTP GET請求)不做限制。
具體的限制規則還有很多,這裡只說常見和本文用得上的。
瀏覽器拒絕
那麼那些環境算是瀏覽器?
- PC端常見的 Chrome/Safari/Edge
- 移動端的Chrome/Safari/各個App內嵌Webview 瀏覽器又是怎麼拒絕的 先來看一張圖,一個使用者點選了一個按鈕,發出了一個AJAX GET請求。那麼常見的流程如圖:
那麼如果使用者發出的AJAX GET請求是一個跨域請求,那麼會在上圖中哪一個階段被阻止? 但是第3階段,也就是說使用者傳送的資訊可以到達服務端,伺服器是能夠接受處理並返回了。返回的瀏覽器發現這是一個跨域請求。就直接拒絕,同時把返回的資訊替換為報錯資訊,返回給JavaScript程式。 對於更復雜的POST/PUT等請求, MDN CORS文件裡面有更詳細的處理方法。這裡就不細說。
這一點很重要,但是總是被新人忽略。所以重要的事情說三遍,
- 拒絕跨域請求是瀏覽器
- 拒絕跨域請求是瀏覽器
- 拒絕跨域請求是瀏覽器
反過來說,Nginx、Java/Nodejs等程式語言的HTTPClient以及手機App,他們發出的HTTP請求就完全沒有跨域問題,因為他們不是瀏覽器,沒有實現W3C規範。
跨域解決方案
JSONP
在瀏覽器中假設有以下一段程式碼會執行結果會是什麼樣?
<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
- 在window物件上掛載一個函式callbackFun
- 建立一個script標籤:
<script src="http://api.domain.com/v1/users/1?callback=callbackFun"></script>
- script就會向伺服器發出
GET http://api.domain.com/v1/users/1?callback=callbackFun
的請求 - 讓後端返回如下內容 ContentType為
application/javascript
callbackFun({/*需要的資料*/});
複製程式碼
- 資料返回成功以後處理資料,刪除script標籤
以上步驟就是JSONP的思想。實現一個完善的JSNOP請求庫還有細節要處理,比如超時取消、回撥函式防重名等。很多開源庫(jQuery, axios)都實現了JSNOP請求。想要程式碼的去Github閱讀原始碼,這裡就不給出程式碼。
優劣勢
JSONP雖然是一種實現跨域訪問的方法,前端想要使用JSONP進行跨域訪問卻不容易。
- 只支援GET方法
- 要後端的配合
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個問題
- 頁面沒辦法訪問
- 介面跨域導致沒辦法訪問
對於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完成。
缺點就是
- 瀏覽器相容性差
- 降低了安全性,畢竟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介面。其他任何情況下不推薦。