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