Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
域(Cross-origin)是 Web 開(kāi)發(fā)中經(jīng)常會(huì)遇到的一種問(wèn)題,本文將徹底剖析前端開(kāi)發(fā)中跨域問(wèn)題的各種表現(xiàn)和原因,深入探討幾種跨域解決方案,并提供對(duì)應(yīng)的示例代碼。
一、什么是跨域
同源策略(Same-origin policy)是 Web 安全的重要策略之一,它規(guī)定了不同源之間的明確分界線,源指的是協(xié)議、主機(jī)和端口號(hào)的組合。
同源策略的實(shí)質(zhì)是一個(gè)域下的文檔或腳本不能獲取另一個(gè)域下的內(nèi)容。但是網(wǎng)絡(luò)上存在著很多需要進(jìn)行跨域操作才能正常訪問(wèn)的網(wǎng)頁(yè),比如 CDN、第三方登錄、跨域 AJAX、IFrame 和跨域資源嵌入等等。
二、跨域表現(xiàn)形式
1. AJAX 跨域:
XMLHttpRequest 和 Fetch 在發(fā)起跨域請(qǐng)求時(shí)會(huì)出現(xiàn)如下異常:
XMLHttpRequest cannot load http://example.com/path. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access.
2. 動(dòng)態(tài)添加 script 標(biāo)簽跨域:
動(dòng)態(tài)添加的 script 標(biāo)簽可以跨域訪問(wèn)資源,但能獲取到的數(shù)據(jù)受到服務(wù)器數(shù)據(jù)格式及隱私策略的限制,此方法一般用于實(shí)現(xiàn) JSONP 解決跨域問(wèn)題。
3. 資源跨域嵌入:
在 HTML 代碼中,使用跨域資源嵌入(Cross-origin Resource Embedding, CORS)實(shí)現(xiàn)跨域,瀏覽器會(huì)發(fā)起預(yù)檢請(qǐng)求(Options),詢(xún)問(wèn)服務(wù)器是否支持特定的跨域請(qǐng)求,隨后發(fā)起真正的請(qǐng)求,如果發(fā)現(xiàn)請(qǐng)求的資源沒(méi)有跨域限制,則會(huì)正常訪問(wèn),否則會(huì)出現(xiàn)跨域異常。
三、跨域解決方案
1. JSONP
JSONP(JSON with Padding),是通過(guò)把 JSON 數(shù)據(jù)作為參數(shù)傳遞到一個(gè)函數(shù)中,該函數(shù)在回調(diào)中處理 JSON 數(shù)據(jù)。一般來(lái)說(shuō),JSONP 數(shù)據(jù)的傳輸方式是通過(guò) script 標(biāo)簽的 src 屬性進(jìn)行跨域請(qǐng)求,因?yàn)?script 標(biāo)簽的跨域限制很少。
服務(wù)器返回的數(shù)據(jù)格式應(yīng)該是以下的格式:
callbackName({"name": "Lucy", "age": 18})
實(shí)現(xiàn)一個(gè)簡(jiǎn)單的 JSONP:
function createJsonp(url, callback) {
const script=document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('src', url);
document.body.appendChild(script);
window[callback]=function(data) {
callback(data);
document.body.removeChild(script);
}
}
調(diào)用方式:
createJsonp('http://example.com/api?callback=showData', 'showData');
2. CORS
CORS 是解決跨域最常用的解決方案之一,其原理是服務(wù)器在響應(yīng)中添加如下 Header 信息:
Access-Control-Allow-Origin: http://localhost:8000
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin 表示允許訪問(wèn)的來(lái)源地址,其取值可以是 *,表示允許來(lái)自任何地址的請(qǐng)求。
Access-Control-Allow-Methods 表示允許的 HTTP 請(qǐng)求方法。
Access-Control-Allow-Headers 表示允許的 HTTP 請(qǐng)求頭。
Access-Control-Allow-Credentials 表示是否允許攜帶憑證信息,如 cookie。
客戶端的請(qǐng)求代碼:
const xhr=new XMLHttpRequest();
xhr.open('GET', 'http://example.com/api', true);
xhr.withCredentials=true; // 攜帶 cookie
xhr.onload=function() {
console.log(xhr.responseText);
}
xhr.send();
3. postMessage
由于同源策略的限制,在一個(gè)頁(yè)面中嵌入另一個(gè)頁(yè)面無(wú)法直接訪問(wèn)并操作另一個(gè)頁(yè)面的 DOM 元素,為了解決這一問(wèn)題,可以使用 postMessage 來(lái)實(shí)現(xiàn)跨域通信。
代碼示例:
// 主頁(yè)面:http://localhost:8000/main
window.onload=function() {
const iframe=document.createElement('iframe');
iframe.setAttribute('src', 'http://example.com/iframe.html');
iframe.style.display='none';
document.body.appendChild(iframe);
window.addEventListener('message', function(event) {
console.log(event.origin);
console.log(event.data);
});
}
//iframe頁(yè)面:http://example.com/iframe.html
const targetOrigin='http://localhost:8000';
window.parent.postMessage('message from iframe', targetOrigin);
四、總結(jié)
跨域問(wèn)題不是前端開(kāi)發(fā)人員可以繞過(guò)的,因?yàn)椴煌膱?chǎng)合會(huì)有不同的解決方案,常見(jiàn)的解決方案有 JSONP、CORS 和 postMessage。所以熟悉 Web 安全,了解跨域問(wèn)題的知識(shí)顯得至關(guān)重要,如果你能夠深入理解和掌握跨域問(wèn)題,相信你在 Web 開(kāi)發(fā)領(lǐng)域的技術(shù)影響力也會(huì)更大。
本期文章的視頻內(nèi)容可以去 B 站搜索看,視頻內(nèi)容有完整的分析和演示
在 BS 架構(gòu)的項(xiàng)目中以及前端開(kāi)發(fā)編碼過(guò)程中有可能會(huì)遇到跨域的問(wèn)題,而跨域問(wèn)題隨著現(xiàn)代 web 開(kāi)發(fā)很多項(xiàng)目采用前后端分離的方式進(jìn)行分工合作再一次成為前端開(kāi)發(fā)過(guò)程中都會(huì)遇到問(wèn)題。曾經(jīng)有一段時(shí)間 JSONP 的一度成為很多跨域問(wèn)題的解決方案。在個(gè)人印象中曾經(jīng)有一段時(shí)期各個(gè)公司關(guān)注于建站建官網(wǎng)進(jìn)行 SEO,那個(gè)時(shí)候很多開(kāi)發(fā)人員都會(huì)在前端粘貼一段腳本用來(lái)監(jiān)控對(duì)網(wǎng)站的請(qǐng)求,那個(gè)時(shí)候跨域問(wèn)題開(kāi)始進(jìn)入我個(gè)人的視野。后來(lái)隨著 Vue、Angular、React 的興起刮起了前端開(kāi)發(fā)變革的風(fēng),前后分離的開(kāi)發(fā)方式讓跨域問(wèn)題再一次成為需要面對(duì)的問(wèn)題。
所謂跨域問(wèn)題實(shí)際上就是瀏覽器的一個(gè)非常核心的安全策略,有很多方法可以來(lái)解決這個(gè)問(wèn)題,各個(gè)方法使用場(chǎng)景各不相同。在項(xiàng)目中有有使用過(guò)幾個(gè)解決方案,在這里做歸納總結(jié)
在前端開(kāi)發(fā)編碼過(guò)程中,常見(jiàn)的 html 標(biāo)簽例如:a、form、img、script、link、iframe以及 Ajax 操作都可以指向一個(gè)資源地址或者說(shuō)可以發(fā)起對(duì)一個(gè)資源的請(qǐng)求,那么這里所說(shuō)的請(qǐng)求就存在同域請(qǐng)求還是跨域請(qǐng)求。
所謂跨域請(qǐng)求就是指:當(dāng)前發(fā)起請(qǐng)求的域與該請(qǐng)求指向的資源所在的域不一致(這里所有的域是協(xié)議、域名和端口號(hào)的合集,同域就是所協(xié)議、域名和端口號(hào)均相同,任何一個(gè)不同都是跨域)。
常見(jiàn)的跨域場(chǎng)景有:
常見(jiàn)的跨域場(chǎng)景
同源策略是由 Netscape 提出的一個(gè)著名的安全策略,它是瀏覽器最基本最核心的安全功能。如果缺少了同源策略,則瀏覽器的正常功能都會(huì)受到影響,可以說(shuō) web 是構(gòu)建在同源策略基礎(chǔ)上的,瀏覽器是針對(duì)同源策略的一種實(shí)現(xiàn)。
同源策略是瀏覽器的行為,是為了保護(hù)本地?cái)?shù)據(jù)不被 Javascript 代碼獲取回來(lái)的數(shù)據(jù)污染,因此攔截的是客戶端發(fā)出請(qǐng)求后回來(lái)的數(shù)據(jù)接收,即請(qǐng)求發(fā)送了,服務(wù)器響應(yīng)了,但無(wú)法被瀏覽器接收?qǐng)?zhí)行,在現(xiàn)代瀏覽器中違反同源策略的跨域請(qǐng)求會(huì)在控制臺(tái)直接報(bào)錯(cuò)。
chrome控制臺(tái)報(bào)錯(cuò)信息
同源策略的具體表現(xiàn):
為什么會(huì)有這些限制
同源策略是瀏覽器最核心基礎(chǔ)的一個(gè)安全策略,是瀏覽器基于安全的需要來(lái)進(jìn)行的限制。常見(jiàn)的CSRF、XSS攻擊都與之有關(guān)聯(lián)。在這里就不做介紹,有興趣的讀者可以自行去搜索了解下這些攻擊的原理
既然同源策略是瀏覽器的核心基礎(chǔ)安全策略,那為什么我們?cè)谶M(jìn)行前端開(kāi)發(fā)特別是 Ajax 調(diào)用時(shí)還要進(jìn)行跨域請(qǐng)求呢?同源策略是用來(lái)防御來(lái)自非法的攻擊,但我們不能因?yàn)榉烙欠ǖ墓艟蛯⑺械目缬蚨紨r截掉。
在現(xiàn)代前端開(kāi)發(fā)中,我們經(jīng)常需要調(diào)用第三方的服務(wù)接口(例如 mock server、fake api),隨著專(zhuān)業(yè)化分工的出現(xiàn)有很多專(zhuān)業(yè)的信息服務(wù)提供商為前端開(kāi)發(fā)者提供各類(lèi)接口,這種情況下就需要進(jìn)行跨域請(qǐng)求(這類(lèi)前端接口服務(wù)很多是采用的 cors 方式來(lái)解決跨域問(wèn)題的,下文會(huì)詳細(xì)介紹)。
還有一類(lèi)情況是在前后端分離的項(xiàng)目中,前端后端分屬于不同的服務(wù)跨域問(wèn)題在采用這種架構(gòu)的時(shí)候就存在。而且現(xiàn)在很多項(xiàng)目都采用這種前后分離的方式,這類(lèi)項(xiàng)目很多是會(huì)采用反向代理的方式來(lái)解決跨域問(wèn)題。
這里所說(shuō)的跨域請(qǐng)求解決方案主要是針對(duì) Ajax 請(qǐng)求
修改瀏覽器的安全設(shè)置(不推薦)
既然是瀏覽器的安全策略,那么最簡(jiǎn)單粗暴的方法就是禁用這個(gè)策略。這種操作很危險(xiǎn),不推薦使用。在此也不做具體的介紹,在跨域問(wèn)題剛出來(lái)的時(shí)候有人采用這種方法,目前已很少有人采用。
JSONP
JSONP(JSON with Padding)是 JSON 的一種使用模式,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問(wèn)問(wèn)題,在早兩三年前端解決跨域問(wèn)題中經(jīng)常出現(xiàn)這類(lèi)解決方案。
JSONP 的原理就是,Ajax 存在跨域安全問(wèn)題但是 script 標(biāo)簽是不存在這類(lèi)問(wèn)題的,于是乎就有人根據(jù)這個(gè)特性做文章找解決方案。
remote.com/remote.js
remoteFunction('remote)
index.html
<script src="http://remote.com/remote.js" type="text/javascript"></script> <script> remoteFunction() </script>
我們都知道這種方式是可以成功的,因此進(jìn)一步改造:
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>
通過(guò)這一步的改造就可以發(fā)現(xiàn)我們的本地函數(shù)的參數(shù)是來(lái)自于遠(yuǎn)程服務(wù),在遠(yuǎn)程 remote.js 根據(jù)業(yè)務(wù)邏輯傳遞參數(shù)到本地函數(shù)進(jìn)行的調(diào)用。
于是更進(jìn)一步,將代碼寫(xiě)死的 remote.js 進(jìn)行動(dòng)態(tài)的生成,我們以后臺(tái) PHP 語(yǔ)言為例:
remote.php
<?php //通過(guò)設(shè)置content-type能夠指明返回的內(nèi)容類(lè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>
這種實(shí)現(xiàn)方式就是 JSONP 的簡(jiǎn)單實(shí)現(xiàn),JSONP 的核心理念就是利用 script 可以進(jìn)行跨域請(qǐng)求,通過(guò)跨域請(qǐng)求將業(yè)務(wù)處理邏輯的回調(diào)函數(shù)通過(guò) url 參數(shù)的形式發(fā)給遠(yuǎn)程請(qǐng)求,遠(yuǎn)程請(qǐng)求通過(guò)數(shù)據(jù)庫(kù)調(diào)用獲取到前端需要的數(shù)據(jù)后,將發(fā)送過(guò)來(lái)的回掉函數(shù)以及數(shù)據(jù)參數(shù)進(jìn)行拼裝,生成一段可執(zhí)行的 JavaScript(json)代碼,這樣當(dāng)這次請(qǐng)求完成后,對(duì)應(yīng)的業(yè)務(wù)處理函數(shù)也就對(duì)應(yīng)的執(zhí)行了。
JSONP 有其天然的缺陷,因?yàn)槭峭ㄟ^(guò) script 的 src 進(jìn)行的資源請(qǐng)求,所以都是 GET 方式,其他的 POST、PUT、DELETE 并不支持。這種采用這種解決方式越來(lái)越少。
跨域資源共享 CORS(Cross-Origin Resource Sharing)
CORS 是一個(gè)新的 W3C 標(biāo)準(zhǔn),它新增的一組 HTTP 首部字段允許服務(wù)器其聲明哪些來(lái)源請(qǐng)求有權(quán)限訪問(wèn)哪些資源,換言之它允許瀏覽器向其聲明了 CORS 的站進(jìn)行跨域請(qǐng)求。
這種方式最主要的特點(diǎn)就是會(huì)在響應(yīng)的 HTTP 首部增加 Access-Control-Allow-Origin 等信息,從而判定哪些資源站可以進(jìn)行跨域請(qǐng)求,還有幾個(gè)其他相關(guān)的 HTTP 首部進(jìn)行更加精細(xì)化的控制,最主要的還是 Access-Control-Allow-Origin。具體每個(gè)首部信息的含義可以去搜索詳細(xì)了解下。
我們以 Express 搭建的遠(yuǎn)程服務(wù)為例來(lái)說(shuō)明:
var express=require("express"); var cors=require("cors"); var app=express(); //使用express的cors中間件使其支持跨域請(qǐng)求 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"); });
針對(duì)支持 CORS 的服務(wù)發(fā)起 Ajax 請(qǐng)求最大的特定,客戶端即瀏覽器首先會(huì)發(fā)送一次請(qǐng)求到服務(wù)端判斷服務(wù)端是否支持跨域請(qǐng)求及是否合法,如果判斷通過(guò)會(huì)回復(fù)信息給客戶端瀏覽器,瀏覽器通過(guò)收到的回復(fù)信息判斷服務(wù)端對(duì)這次跨域請(qǐng)求是否支持,如果支持就再發(fā)送實(shí)際的業(yè)務(wù)請(qǐng)求。所以在這里會(huì)有兩次請(qǐng)求。
CORS 與 JSONP 對(duì)比來(lái)說(shuō)優(yōu)勢(shì)比較明顯,JSONP 只支持 GET 方式局限性很多,而且 JSONP 并不符合處理業(yè)務(wù)的正常流程。采用 CORS 的方式,前端編碼與正常非跨域請(qǐng)求沒(méi)有什么不同。在目前很多的 Fake API (模擬接口服務(wù))、Mock Server(數(shù)據(jù)模擬服務(wù))以及其他公共服務(wù)上都很多采用 CORS 的方式來(lái)解決跨域問(wèn)題,例如 json-server 等。
iframe
iframe 與 JSONP 都是使用 src 屬性沒(méi)有跨域限制的特性,iframe 這種方式也不推薦使用,不做詳細(xì)介紹。
反向代理
既然不能跨域請(qǐng)求,那么我們不跨域就可以了。通過(guò)在請(qǐng)求到達(dá)服務(wù)前部署一個(gè)服務(wù),將接口請(qǐng)求進(jìn)行轉(zhuǎn)發(fā),這就是反向代理。通過(guò)一定的轉(zhuǎn)發(fā)規(guī)則可以將前端的請(qǐng)求轉(zhuǎn)發(fā)到其他的服務(wù)。以 Nginx 為例:
server { listen 9999 server_name localhost #將所有l(wèi)ocalhost:9099/api為開(kāi)頭的請(qǐng)求進(jìn)行轉(zhuǎn)發(fā) location ^~ /api { proxy_pass http://localhost:3000; } }
通過(guò)反向代理我們將前端后端項(xiàng)目統(tǒng)一通過(guò)反向代理來(lái)提供對(duì)外的服務(wù),這樣在前端看上去就跟不存在跨域一樣。
反向代理麻煩之處就在原對(duì) Nginx 等反向代理服務(wù)的配置,在目前前后端分離的項(xiàng)目中很多都是采用這種方式。
綜上所述,CORS 和反向代理是目前使用最多的解決方案,這兩個(gè)解決方案使用的場(chǎng)景并不相同,我們要根據(jù)自身的需求進(jìn)行選擇。公共服務(wù)、Fake API 、Mock Server 一般采用 CORS 的方案;而公司前后端分離的項(xiàng)目中更多是采用反向代理的方案。
前端編程中,跨域問(wèn)題應(yīng)該是很常見(jiàn)的,處理方式有很多,下邊來(lái)說(shuō)一說(shuō)用到過(guò)的處理方式。
什么是跨域:
只要協(xié)議、域名、端口有任何一個(gè)不同,都被當(dāng)做不同的域,js不能在不同的域之間進(jìn)行通信和傳輸數(shù)據(jù)。
跨域的情況:
1、用ajax向不同的域請(qǐng)求數(shù)據(jù)
2、通過(guò)js獲取頁(yè)面中不同域的框架中的數(shù)據(jù)(常見(jiàn)iframe)
瀏覽器都有一個(gè)同源策略,其限制之一就是不能通過(guò)ajax的方法去請(qǐng)求不同源中的文檔,限制之二是瀏覽器中不同域的框架之間是不能進(jìn)行js的交互操作的。
跨域的方法:1、 jsonp跨域原理:創(chuàng)建<script>標(biāo)簽,利用src屬性跨域(src屬性可以跨域),同樣<img>也可以處理跨域例子:test.html -----> http://a.haha.com/test.htmlajaxData -----> http://b.haha.com/listtest.html訪問(wèn)ajaxData需要跨域
通過(guò)一個(gè)script標(biāo)簽引入一個(gè)js文件,當(dāng)js文件載入成功后會(huì)把需要的json數(shù)據(jù)作為參數(shù)傳入U(xiǎn)RL中指定的函數(shù)并執(zhí)行此函數(shù),因?yàn)閍jaxData被當(dāng)作一個(gè)js文件來(lái)引入,所以其返回的數(shù)據(jù)必須是一個(gè)能執(zhí)行的js文件,所以需要服務(wù)端的配合才行。
局限性: 需要服務(wù)端配合做處理 jsonp只支持“get”請(qǐng)求,不支持“post”請(qǐng)求
2、 document.domain來(lái)跨越子域
原理:設(shè)置相同的主域例子:一個(gè)頁(yè)面,它的地址是 http://a.haha.com/test.html , 在這個(gè)頁(yè)面里面有一個(gè)iframe,它的src是 http://b.haha.com/test.html , 很顯然,這個(gè)頁(yè)面與它里面的iframe框架是不同域的,所以我們是無(wú)法通過(guò)在頁(yè)面中書(shū)寫(xiě)js代碼來(lái)獲取iframe中的東西的
document.domain的設(shè)置是有限制的,我們只能把document.domain設(shè)置成自身或更高一級(jí)的父域,且主域必須相同。
修改document.domain的方法只適用于不同子域的框架間的交互,對(duì)ajax訪問(wèn)的不適用。
3、隱藏iframe做代理跨域
如果你想通過(guò)ajax的方法去與不同子域的頁(yè)面交互,除了使用jsonp的方法外,還可以用一個(gè)隱藏的iframe來(lái)做一個(gè)代理。
原理:讓這個(gè)隱藏的iframe載入一個(gè)與你想要通過(guò)ajax獲取數(shù)據(jù)的目標(biāo)頁(yè)面處在相同的域的頁(yè)面,所以這個(gè)iframe中的頁(yè)面是可以正常使用ajax去獲取你要的數(shù)據(jù)的,然后就是通過(guò)修改document.domain的方法,讓我們能通過(guò)js完全控制這個(gè)iframe,這樣我們就可以讓iframe去發(fā)送ajax請(qǐng)求,然后收到的數(shù)據(jù)我們也可以獲得了。
以上第3種方式是比較常見(jiàn)的,也是用的比較多的,當(dāng)然跨域方式有好多種,歡迎有興趣的小伙伴一起討論。
*請(qǐng)認(rèn)真填寫(xiě)需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。