你所需要的跨域問題的全套解決方案都在這裏啦!(前後端都有)
導論
隨着RESTful架構風格成爲主流,以及Vue.js、React.js和Angular.js這三大前端框架的日益強大,越來越多的開發者開始由傳統的MVC架構轉向基於前後端分離這一基礎架構來構建自己的系統,將前端頁面和後端服務分別部署在不同的域名之下。在此過程中一個重要的問題就是跨域資源訪問的問題,通常由於同域安全策略瀏覽器會攔截JavaScript腳本的跨域網絡請求,這也就造成了系統上線時前端無法訪問後端資源這一問題。筆者將結合自身開發經驗,對這一問題產生的原因以及相應的解決方案,給出詳細介紹。
問題原因
同源策略
同源策略,它是由Netscape提出的一個著名的安全策略。現在所有支持JavaScript 的瀏覽器都會使用這個策略。所謂同源是指:協議、域名、端口相同。
舉個例子:
url | url | 是否同源 |
---|---|---|
http://test001.com/ | https://test001.com/ | 協議不同 非同源 |
http://test001.com/ | http://test002.com/ | 域名不同 非同源 |
http://test001.com:3000 | http://test001.com:4000 | 端口不同 非同源 |
http://test001.com/userList | http://test001.com/orderList | 同源 |
一個瀏覽器的兩個tab頁中分別打開來百度和谷歌的頁面,當瀏覽器的百度tab頁執行一個腳本的時候會檢查這個腳本是屬於哪個頁面的,即檢查是否同源,只有和百度同源的腳本纔會被執行。如果非同源,那麼在請求數據時,瀏覽器會在控制檯中報一個異常,提示拒絕訪問。同源策略是瀏覽器的行爲,是爲了保護本地數據不被JavaScript代碼獲取回來的數據污染,因此攔截的是客戶端發出的請求回來的數據接收,即請求發送了,服務器響應了,但是無法被瀏覽器接收。
現象分析
在前端開發階段,一些框架的腳手架工具會使用webpack-dev-serve來代理數據請求,其本質上是一個基於node.js的網頁服務器,所以感受不到跨域資源訪問的限制。
當網站上線後,網頁上很多資源都是要通過發送AJAX請求向服務器索要資源,但是在前後端分離的系統架構中,前端頁面和後端服務往往不會部署在同一域名之下。比如用戶通過瀏覽器訪問 http://www.test001.com 這一地址,來到了系統首頁,此時瀏覽器從網站服務器中只取回了基本的HTML頁面以及CSS樣式表文件和JavaScript腳本。系統首頁的其他內容,比如輪播圖、文章列表等,需要利用JavaScript腳本程序,向地址爲 http://www.test002.com 的後端應用服務器發送請求來獲取信息。此時由於瀏覽器的同源策略,該請求會被瀏覽器所攔截,這就造成了前後端數據不通這一結果。
解決方案
前端解決方案
反向代理
因爲由於瀏覽器的同源策略,JavaScript腳本程序只能向同一域名下的服務器發送網絡請求,那麼可以通過網頁服務器轉發這一網絡請求到相應的後端服務器,獲取相關數據,然後網頁服務器再把這一數據返回給瀏覽器。這一過程稱之爲反向代理。
假設用戶通過地址http://www.test001.com訪問到了系統首頁,該系統首頁中所加載的JavaScript腳步程序本應該要發送AJAX請求到http://www.test002.com/api/articleList這一地址,來獲取首頁文章列表信息。此時應該改成向http://www.test001.com/api/articleList這一與之同源的地址發送數據請求。該系統的網頁服務器會收到此請求,然後代替JavaScript腳本程序向http://www.test002.com/api/articleList這一地址請求數據,獲取數據後將之返回給瀏覽器。此時JavaScript腳本程序就通過網頁服務器這一橋樑成功獲取到了後端應用服務器上的數據。
若服務器採用了寶塔面板這一管理軟件,可以直接通過其提供的可視化界面進行反向代理的設置。對於一些新手而言,直接面對命令行進行各種操作,不夠直觀且難度較高,此時採用一些可視化的服務器管理軟件是一個不錯的選擇。
若是喜歡用vim 直接在命令行裏修改的同學可以參考這篇博客
這個解決方案是不是有些眼熟呢?
JSONP跨域
瀏覽器的同源策略對JavaScript腳本向不同域的服務器請求數據進行了限制,但是沒有對HTML中的<script>標籤進行限制,我們可以基於此規則,動態創建<script>標籤進行跨域資源訪問。<script>標籤中src這一屬性值設置爲:接口地址+處理數據的回調函數名稱。相關代碼示例如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>JSONP 跨域演示</title>
</head>
<body>
<script>
// 先定義好回調函數
function getarticleList(res) {
console.log(res);
}
var script = document.createElement('script');
script.type = 'text/javascript';
// 設置接口地址+數據獲取成功後的回調函數(handleData)
script.src = 'http://localhost:3088/articleList&callback=getarticleList';
document.body.appendChild(script);
</script>
</body>
</html>
後端的話需要根據前端所傳遞過來的回調函數名稱,把數據封裝在此函數裏面,這樣在前端加載好數據後就自動調用了回調函數進行數據處理。(這是個騷操作)後端代碼示例如下:
let http = require("http");
let querystring = require("querystring");
let server = http.createServer();
let articleList = JSON.stringify([
{
id: 1,
name: "平凡的世界"
},
{
id: 2,
name: "時間簡史"
}
]);
server.on("request", function(req, res) {
console.log("收到請求了,請求路徑是:" + req.url);
console.log(
"callback function name:",
JSON.stringify(querystring.parse(req.url))
);
let callbackFunctionName = querystring.parse(req.url).callback;
//根據回調函數名稱,封裝數據
let result = `${callbackFunctionName}(${articleList})`;
res.end(result);
});
server.listen(3088);
若有些朋友雲裏霧裏的話,可以參考JSONP - 從理論到實踐此篇博文
在這裏值得注意的是,因爲請求數據的接口地址是寫在了<script>標籤中src這一屬性值裏面,那麼數據請求的方式就只能支持GET請求,其他請求無法實現。在基於Vue.js這種框架開發的項目中,因爲其使用了虛擬化DOM這一概念,JSONP跨域的方式對其並不是一個很好的選擇,對於原生JavaScript代碼,可以採用此方式進行跨域。
後端解決方案
跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個origin (domain)上的Web應用被准許訪問來自不同源服務器上的指定的資源。
出於安全原因,瀏覽器限制從腳本內發起的跨源HTTP請求。 例如,XMLHttpRequest和Fetch API遵循同源策略。 這意味着使用這些API的Web應用程序只能從加載應用程序的同一個域請求HTTP資源,除非響應報文包含了正確CORS響應頭! 所以要想實現跨域資源訪問,這也就要求後端服務程序,應該根據CORS策略來配置好相應的HTTP響應頭。
Access-Control-Allow-Origin: *
表示該資源可以被任意外域訪問。
如果服務端僅允許來自 http://test001.com 的訪問,該首部字段的內容如下:
Access-Control-Allow-Origin: http://test001.com
Express
在 Node.js 的輕量級 Web 框架 Express 中,我們只需要安裝一個 cors 庫並添加此中間件即可配置好跨域問題:
npm install cors
然後在 Express 應用中使用這個中間件:
var express = require('express')
var cors = require('cors')
var app = express()
app.use(cors())
app.get('/products/:id', function (req, res, next) {
res.json({msg: 'This is CORS-enabled for all origins!'})
})
app.listen(80, function () {
console.log('CORS-enabled web server listening on port 80')
})
通過這樣的方式,就允許了所有的域名的請求方法。更多針對單個路由的跨域控制可以參見 cors 文檔。
SpringBoot
在以SpringBoot爲基礎框架的應用程序中可以增加一個配置類進行CORS配置。具體代碼如下所示:
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Autowired
AdminInterceptor adminInterceptor;
//配置跨域相關
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("*")
.allowedOrigins("*")
.allowedHeaders("*");
super.addCorsMappings(registry);
}
}
上述代碼是較爲粗獷的解決方案,即允許了所有域名的所有請求方法。在實際開發過程中應對於所收到請求的請求路徑、請求方法、源、請求頭加以限制,以確保服務的安全。繼續以上述例子說明,安全的配置應該如下:
//配置跨域相關
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedMethods("GET")
.allowedOrigins("www.test001.com")
.allowedHeaders("*");
super.addCorsMappings(registry);
}
在這種配置中只有來自域名www.test001.com纔可以訪問服務器數據,而且只接受GET方式的數據請求,對於訪問路徑也做了限制,只有/api開頭的路徑才能訪問的到。這樣就進一步保證了後端應用程序的安全性。
Flask
在以Flask這一輕量級web服務框架爲基礎所開發的應用服務中,首先要安裝flask跨域資源共享庫,可使用命令pip install flask_cors。接下來可按照如下代碼進行CORS配置。
from flask_cors import CORS
app = Flask(__name__)
CORS(app, supports_credentials=True)
總結
跨域問題在目前後端分離的架構中普遍存在,本文所介紹的這幾種方案雖然都能夠解決跨域問題,但其實各有優劣。比如Jsonp方式實現起來較爲簡單,但只支持GET請求方式,在原生JavaScript腳本中使用方便,但是當利用瞭如Vue.js這種MVVM框架時就有些難以施展了。反向代理的方式無需改動後端代碼,但是對於整個系統而言可移植性較差,CORS方式需要後端來積極配合前端實現跨域。總之,沒有技術銀彈,我們要在實際情形中比較分析,選擇最合適的方案。