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