前言
本期文章的視頻內(nèi)容可以去 B 站看,視頻內(nèi)容有完整的代碼演示
在 BS 架構(gòu)的項目中以及前端開發(fā)編碼過程中有可能會遇到跨域的問題,而跨域問題隨著現(xiàn)代 web 開發(fā)很多項目采用前后端分離的方式進行分工合作再一次成為前端開發(fā)過程中都會遇到問題。曾經(jīng)有一段時間 JSONP 的一度成為很多跨域問題的解決方案。在個人印象中曾經(jīng)有一段時期各個公司關(guān)注于建站建官網(wǎng)進行 SEO,那個時候很多開發(fā)人員都會在前端粘貼一段腳本用來監(jiān)控對網(wǎng)站的請求,那個時候跨域問題開始進入我個人的視野。后來隨著 Vue、Angular、React 的興起刮起了前端開發(fā)變革的風,前后分離的開發(fā)方式讓跨域問題再一次成為需要面對的問題。
所謂跨域問題實際上就是瀏覽器的一個非常核心的安全策略,有很多方法可以來解決這個問題,各個方法使用場景各不相同。在項目中有有使用過幾個解決方案,在這里做歸納總結(jié)
什么是跨域請求
在前端開發(fā)編碼過程中,常見的 html 標簽例如:a、form、img、script、link、iframe以及 Ajax 操作都可以指向一個資源地址或者說可以發(fā)起對一個資源的請求,那么這里所說的請求就存在同域請求還是跨域請求。
所謂跨域請求就是指:當前發(fā)起請求的域與該請求指向的資源所在的域不一致(這里所有的域是協(xié)議、域名和端口號的合集,同域就是所協(xié)議、域名和端口號均相同,任何一個不同都是跨域)。
常見的跨域場景有:
| 源 URL | 請求 URL | 是否跨域 | 說明 |
|---|---|---|---|
| http://a.com/a | http://a.com/b | 否 | 同協(xié)議同域名通端口號,不同資源請求,不是跨域請求 |
| http://a.com/a | http://a.com:8080/b | 是 | 端口不同 |
| http://a.com/a | https://a.com/b | 是 | 協(xié)議不同 |
| http://www.a.com/a | http://x.a.com/b | 是 | 主域名相同,但是子域名不相同 |
| http://a.com/a | http://b.com/b | 是 | 域名不相同 |
同源策略(same-origin pllicy)
同源策略是由 Netscape 提出的一個著名的安全策略,它是瀏覽器最基本最核心的安全功能。如果缺少了同源策略,則瀏覽器的正常功能都會受到影響,可以說 web 是構(gòu)建在同源策略基礎(chǔ)上的,瀏覽器是針對同源策略的一種實現(xiàn)。
同源策略是瀏覽器的行為,是為了保護本地數(shù)據(jù)不被 Javascript 代碼獲取回來的數(shù)據(jù)污染,因此攔截的是客戶端發(fā)出請求后回來的數(shù)據(jù)接收,即請求發(fā)送了,服務(wù)器響應(yīng)了,但無法被瀏覽器接收執(zhí)行,在現(xiàn)代瀏覽器中違反同源策略的跨域請求會在控制臺直接報錯。
[站外圖片上傳中...(image-8980a0-1561169393474)]
同源策略的具體表現(xiàn):
- DOM 層面的同源策略,限制了來自不同源的
Document對象或者 JS 腳本對當前Document對象的讀取或設(shè)置某些屬性 - Cookie 和 XMLHttpRequest 層面的同源策略,默認情況下禁止 Ajax 直接進行跨域請求(請求可以發(fā)起,但接收結(jié)果等操作被瀏覽器攔截并在控制臺報錯),cookie 層面數(shù)據(jù)默認不能跨域訪問
- 特定 HTML 標簽的的策略,a、form、img、script、link、iframe等這些有
src屬性的標簽可以對資源跨域訪問和執(zhí)行
為什么會有這些限制
同源策略是瀏覽器最核心基礎(chǔ)的一個安全策略,是瀏覽器基于安全的需要來進行的限制。常見的CSRF、XSS攻擊都與之有關(guān)聯(lián)。在這里就不做介紹,有興趣的讀者可以自行去搜索了解下這些攻擊的原理
跨域請求為什么會出現(xiàn)在前端開發(fā)中?
既然同源策略是瀏覽器的核心基礎(chǔ)安全策略,那為什么我們在進行前端開發(fā)特別是 Ajax 調(diào)用時還要進行跨域請求呢?同源策略是用來防御來自非法的攻擊,但我們不能因為防御非法的攻擊就將所有的跨域都攔截掉。
在現(xiàn)代前端開發(fā)中,我們經(jīng)常需要調(diào)用第三方的服務(wù)接口(例如 mock server、fake api),隨著專業(yè)化分工的出現(xiàn)有很多專業(yè)的信息服務(wù)提供商為前端開發(fā)者提供各類接口,這種情況下就需要進行跨域請求(這類前端接口服務(wù)很多是采用的 cors 方式來解決跨域問題的,下文會詳細介紹)。
還有一類情況是在前后端分離的項目中,前端后端分屬于不同的服務(wù)跨域問題在采用這種架構(gòu)的時候就存在。而且現(xiàn)在很多項目都采用這種前后分離的方式,這類項目很多是會采用反向代理的方式來解決跨域問題。
Ajax 跨域請求如何實現(xiàn)
這里所說的跨域請求解決方案主要是針對 Ajax 請求
修改瀏覽器的安全設(shè)置(不推薦)
既然是瀏覽器的安全策略,那么最簡單粗暴的方法就是禁用這個策略。這種操作很危險,不推薦使用。在此也不做具體的介紹,在跨域問題剛出來的時候有人采用這種方法,目前已很少有人采用。
JSONP
JSONP(JSON with Padding)是 JSON 的一種使用模式,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問問題,在早兩三年前端解決跨域問題中經(jīng)常出現(xiàn)這類解決方案。
JSONP 的原理就是,Ajax 存在跨域安全問題但是 script 標簽是不存在這類問題的,于是乎就有人根據(jù)這個特性做文章找解決方案。
remote.com/remote.js
remoteFunction('remote)
index.html
<script src="http://remote.com/remote.js" type="text/javascript"></script>
<script>
remoteFunction()
</script>
我們都知道這種方式是可以成功的,因此進一步改造:
remote.com/remote.js
let data = {
code: 200,
msg: "data from remote"
};
localFunction(data);
index.html
<script>
localFunction(data) {
console.log(data)
}
</script>
<script src="http://remote.com/remote.js" type="text/javascript"></script>
通過這一步的改造就可以發(fā)現(xiàn)我們的本地函數(shù)的參數(shù)是來自于遠程服務(wù),在遠程 remote.js 根據(jù)業(yè)務(wù)邏輯傳遞參數(shù)到本地函數(shù)進行的調(diào)用。
于是更進一步,將代碼寫死的 remote.js 進行動態(tài)的生成,我們以后臺 PHP 語言為例:
remote.php
<?php
//通過設(shè)置content-type能夠指明返回的內(nèi)容類型
header('Contetn-type:application/json');
$callbackFunction = htmlspecialchars($_GET['callback']);
$data = 'data from remote';
echo $callbackFunction.'('.$data.')';
index.html
<script type="text/javascript">
localFunction = function(data) {
console.log(data);
};
</script>
<script
src="http://localhost:3000/remote.php?callback=localFunction"
type="text/javascript"
></script>
這種實現(xiàn)方式就是 JSONP 的簡單實現(xiàn),JSONP 的核心理念就是利用 script 可以進行跨域請求,通過跨域請求將業(yè)務(wù)處理邏輯的回調(diào)函數(shù)通過 url 參數(shù)的形式發(fā)給遠程請求,遠程請求通過數(shù)據(jù)庫調(diào)用獲取到前端需要的數(shù)據(jù)后,將發(fā)送過來的回掉函數(shù)以及數(shù)據(jù)參數(shù)進行拼裝,生成一段可執(zhí)行的 JavaScript(json)代碼,這樣當這次請求完成后,對應(yīng)的業(yè)務(wù)處理函數(shù)也就對應(yīng)的執(zhí)行了。
JSONP 有其天然的缺陷,因為是通過 script 的 src 進行的資源請求,所以都是 GET 方式,其他的 POST、PUT、DELETE 并不支持。這種采用這種解決方式越來越少。
跨域資源共享 CORS(Cross-Origin Resource Sharing)
CORS 是一個新的 W3C 標準,它新增的一組 HTTP 首部字段允許服務(wù)器其聲明哪些來源請求有權(quán)限訪問哪些資源,換言之它允許瀏覽器向其聲明了 CORS 的站進行跨域請求。
這種方式最主要的特點就是會在響應(yīng)的 HTTP 首部增加 Access-Control-Allow-Origin 等信息,從而判定哪些資源站可以進行跨域請求,還有幾個其他相關(guān)的 HTTP 首部進行更加精細化的控制,最主要的還是 Access-Control-Allow-Origin。具體每個首部信息的含義可以去搜索詳細了解下。
我們以 Express 搭建的遠程服務(wù)為例來說明:
var express = require("express");
var cors = require("cors");
var app = express();
//使用express的cors中間件使其支持跨域請求
app.use(cors());
app.get("/", function(req, res, next) {
res.json({ msg: "This is CORS-enabled for all origins!" });
});
app.listen(3000, function() {
console.log("CORS-enabled web server listening on port 80");
});
針對支持 CORS 的服務(wù)發(fā)起 Ajax 請求最大的特定,客戶端即瀏覽器首先會發(fā)送一次請求到服務(wù)端判斷服務(wù)端是否支持跨域請求及是否合法,如果判斷通過會回復信息給客戶端瀏覽器,瀏覽器通過收到的回復信息判斷服務(wù)端對這次跨域請求是否支持,如果支持就再發(fā)送實際的業(yè)務(wù)請求。所以在這里會有兩次請求。
CORS 與 JSONP 對比來說優(yōu)勢比較明顯,JSONP 只支持 GET 方式局限性很多,而且 JSONP 并不符合處理業(yè)務(wù)的正常流程。采用 CORS 的方式,前端編碼與正常非跨域請求沒有什么不同。在目前很多的 Fake API (模擬接口服務(wù))、Mock Server(數(shù)據(jù)模擬服務(wù))以及其他公共服務(wù)上都很多采用 CORS 的方式來解決跨域問題,例如 json-server 等。
iframe
iframe 與 JSONP 都是使用 src 屬性沒有跨域限制的特性,iframe 這種方式也不推薦使用,不做詳細介紹。
反向代理
既然不能跨域請求,那么我們不跨域就可以了。通過在請求到達服務(wù)前部署一個服務(wù),將接口請求進行轉(zhuǎn)發(fā),這就是反向代理。通過一定的轉(zhuǎn)發(fā)規(guī)則可以將前端的請求轉(zhuǎn)發(fā)到其他的服務(wù)。以 Nginx 為例:
server {
listen 9999
server_name localhost
#將所有l(wèi)ocalhost:9099/api為開頭的請求進行轉(zhuǎn)發(fā)
location ^~ /api {
proxy_pass http://localhost:3000;
}
}
通過反向代理我們將前端后端項目統(tǒng)一通過反向代理來提供對外的服務(wù),這樣在前端看上去就跟不存在跨域一樣。
反向代理麻煩之處就在原對 Nginx 等反向代理服務(wù)的配置,在目前前后端分離的項目中很多都是采用這種方式。
總結(jié)
綜上所述,CORS 和反向代理是目前使用最多的解決方案,這兩個解決方案使用的場景并不相同,我們要根據(jù)自身的需求進行選擇。公共服務(wù)、Fake API 、Mock Server 一般采用 CORS 的方案;而公司前后端分離的項目中更多是采用反向代理的方案。