在前端的開發中,跨域問題非常常見,尤其是在涉及到前後端分離的應用中更為常見。我在最近的面試當中也會被常常問到,所以我學習了一下,來寫篇文章來加深印象。
首先,我們需要知道為什麼會存在跨域問題
跨域問題的存在主要是出於安全考慮。瀏覽器實施同源策略(Same-origin policy)是爲了防止惡意網站在使用者不知情的情況下,利用使用者的認證資訊(如 Cookie)或者其他敏感資料進行惡意操作。以下是幾個關鍵原因,說明為什麼需要有跨域限制:
安全性
防止資料洩露:同源策略可以防止惡意站點透過指令碼獲取使用者在另一個網站上的敏感資訊。例如,一個惡意站點可以嘗試透過指令碼讀取銀行網站上的使用者賬戶資訊,如果沒有跨域限制,這種攻擊將更容易成功。
防止 CSRF 攻擊:跨站請求偽造(Cross-Site Request Forgery,CSRF)是一種攻擊方式,攻擊者誘導受害者在一個已經登入的 Web 應用程式上執行非本意的操作。同源策略有助於防止這種情況的發生,因為攻擊者的網站無法直接與目標網站進行互動。
保護隱私:跨域限制可以防止第三方網站追蹤使用者的瀏覽習慣。如果沒有跨域限制,第三方網站可以透過指令碼訪問其他網站上的資訊,從而收集使用者的個人資訊。
維護網站邊界
明確資料邊界:同源策略明確了各個網站的資料邊界,使得每個網站負責自己的資料和服務,減少了資料混亂的風險。
減少意外行為:同源策略可以幫助防止一個網站的指令碼意外地影響到另一個網站的功能,保持各自網站的功能獨立性和完整性。
實現機制
同源策略的實現機制是透過瀏覽器來強制執行的。當瀏覽器檢測到請求的源(協議、域名、埠號)與當前頁面的源不同時,就會阻止請求的響應資料被指令碼訪問。
當我們瞭解完跨域問題的本質後,來說說如何來解決跨域
解決跨域的方式有挺多種的,我今天要說的有“JSONP,CORS,代理伺服器,postMessage,web Sorket” 接下來我一個一個的講解
首先是JSONP
JSONP
我們在寫html檔案的時候,經常會使用到script標籤的src屬性去請求外部的CDN,誒,既然是請求,那不就應該會同源策略的影響嗎。經過一系列的嘗試,發現它不會受同源策略的影響,至於原因嘛,我去問了下ai,它總結的比較完善。
為什麼
<script>
標籤不受同源策略影響歷史原因:在同源策略提出之前,
<script>
標籤就已經被廣泛使用了,而且它的設計初衷是爲了讓開發者能夠方便地引入外部指令碼。因此,瀏覽器對<script>
標籤的支援一開始就包含了從任何源載入的能力。安全性和實用性權衡:雖然
<script>
標籤允許從任何源載入指令碼,但瀏覽器依然會對載入的指令碼進行某些安全檢查。例如,瀏覽器會阻止惡意指令碼的執行,尤其是那些試圖進行跨站指令碼攻擊(XSS)的指令碼。此外,現代瀏覽器還會對載入的指令碼進行 CSP(Content Security Policy)檢查,進一步增強了安全性。功能需求:在現代 Web 開發中,很多 JavaScript 框架和庫都是透過
<script>
標籤從 CDN 或其他遠端伺服器載入的。如果<script>
標籤受到同源策略的嚴格限制,將會極大地影響這些框架和庫的使用。
既然script標籤的src不受同源策略的影響,那麼我們不就可以使用它來給傳送請求了嗎。
所以我們可以這樣寫
<script src="http://localhost:3000"></script>
這時候就有小夥伴要問了,可是我們請求介面不是爲了拿到資料嗎,這樣怎麼拿到資料呢。別急接下來我就會告訴你
向後端請求時我們通常會攜帶引數,由於我們利用src來發送請求所以這裏我們使用get請求在請求的url上攜帶引數,我們可以攜帶一個函式,並且要在前端window環境裡去掛載這個函式,在後端寫成呼叫的形式傳給前端,引數就是要傳的資料。這樣的話函式會直接在window環境下直接呼叫,那麼就可以拿到資料了 具體實現如下:
前端
<script> function jsonp(url, cb) { return new Promise(function(resolve, reject) { const script = document.createElement('script'); window[cb] = function(data) { // callback() resolve(data) } script.src = `${url}?cb=${cb}`; document.body.appendChild(script); }); } jsonp('http://localhost:3000', 'callback').then(res => { console.log(res); }) </script>
後端
const http = require('http'); http.createServer(function (req, res) { // res.end('hello world') const query = new URL(req.url,`http://${req.headers.host}`).searchParams const cb = query.get('cb') if(cb) { const data = 'hello world' const result = `${cb}("${data}")` res.end(result) } }).listen(3000);
這就是JSONP的具體實現過程了
但是JSONP有許多缺點,它只能傳送get請求,這就讓資料的傳輸不那麼安全,而且它需要前後端的配合,使用起來比較麻煩,接下來的cors就比較完美了
cors
在前後端專案中,我常常使用cors中介軟體來解決跨域問題,那麼它的原理是什麼呢。
其實它的原理特別簡單,就是在響應頭中新增幾個欄位:
const http = require('http'); http.createServer(function (req, res) { // 開啟cors res.writeHead(200, { // 設定允許的源 'access-control-allow-origin': "*" }) res.end('hello world') }).listen(3000);
關於這些欄位:
Access-Control-Allow-Origin
注意:當設定為 "*" 時,不能包含 Access-Control-Allow-Credentials,因為後者要求源必須是具體的,而不是萬用字元。
用途:指定哪些源可以訪問資源。
示例: "*":表示允許任何源訪問。 "example.com":表示只允許來自 example.com 的請求訪問。 ["example.com", "anotherdomain.com"]:允許多個特定源訪問。
Access-Control-Allow-Credentials
注意:如果設定為 true,則 Access-Control-Allow-Origin 必須是一個具體的源,不能是 "*"。
用途:指示響應是否應該允許包含憑證(如 cookies、HTTP 認證資訊等)。
示例: true:允許包含憑證。 false:不允許包含憑證。
Access-Control-Allow-Methods
用途:指定允許的 HTTP 方法。
示例: "GET, POST, OPTIONS":允許 GET、POST 和 OPTIONS 方法。
Access-Control-Allow-Headers
用途:指定請求中允許的頭部欄位。
示例: "Authorization, X-Requested-With, Content-Type, Accept":允許這些頭部欄位。
Access-Control-Max-Age
用途:指定 Preflight 請求的結果可以被快取的秒數。
示例: 86400:表示結果可以被快取一天(86400 秒)。
Access-Control-Expose-Headers
用途:指定哪些響應頭部欄位可以被客戶端訪問。
示例: "Custom-Header, Another-Header":允許客戶端訪問這些響應頭部欄位。
任何就是postMessgae了
postMessage
這個通常時父級頁面使用postMessage向使用iframe內嵌在自己內部的子級頁面進行相互通訊
postMessage
的基本原理
postMessage
是一種允許兩個不同源的視窗(如兩個不同域名下的視窗)互相傳送訊息的 API。它提供了跨域通訊的能力,同時也可用於同一域名下的不同視窗或 iframe 之間的通訊。
使用 postMessage
的步驟
父頁面傳送訊息
父頁面可以使用 contentWindow.postMessage()
方法向子頁面傳送訊息。這裏 contentWindow
是子頁面的 window
物件,可以透過 iframe 的 contentWindow
屬性獲得。
<!-- 父頁面 index.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Parent Page</title> </head> <body> <iframe id="childFrame" src="child.html"></iframe> <button onclick="sendMessage()">Send Message</button> <script> function sendMessage() { var childWindow = document.getElementById('childFrame').contentWindow; childWindow.postMessage({ type: 'hello', message: 'Hello from parent!' }, '*'); } </script> </body> </html>
子頁面接收訊息
子頁面需要監聽 message
事件,以便接收到來自父頁面的訊息。
<!-- 子頁面 child.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Child Page</title> </head> <body> <script> window.addEventListener('message', function(event) { if (event.origin !== 'http://parentdomain.com') { return; // 只接受來自父域的訊息 } console.log('Received message:', event.data); }); </script> </body> </html>
重要注意事項
來源驗證:接收方應當驗證
event.origin
,確保訊息來自預期的源。這可以防止惡意頁面冒充父頁面傳送訊息。目標源:傳送訊息時,可以指定
targetOrigin
引數來限制訊息只能傳送到特定的源。如果設定為"*"
, 則表示可以傳送到任何源。資料格式:
postMessage
傳送的資料可以是任何可以序列化的 JavaScript 物件,包括字串、陣列、物件等。跨域限制:
postMessage
雖然可以跨域通訊,但仍然受限於同源策略。接收方需要驗證訊息的來源。
示例:雙向通訊
除了父頁面向子頁面傳送訊息外,子頁面也可以向父頁面傳送訊息,並且父頁面也需要監聽 message
事件。
<!-- 父頁面 index.html --> <script> function sendMessage() { var childWindow = document.getElementById('childFrame').contentWindow; childWindow.postMessage({ type: 'hello', message: 'Hello from parent!' }, '*'); } window.addEventListener('message', function(event) { if (event.origin !== 'http://childdomain.com') { return; // 只接受來自子域的訊息 } console.log('Received message:', event.data); }); </script>
<!-- 子頁面 child.html --> <script> window.addEventListener('load', function() { var parentWindow = window.parent; parentWindow.postMessage({ type: 'reply', message: 'Reply from child!' }, '*'); }); window.addEventListener('message', function(event) { if (event.origin !== 'http://parentdomain.com') { return; // 只接受來自父域的訊息 } console.log('Received message:', event.data); }); </script>
然後是webSocket
webSocket
webSocket 它是socket協議本身就不受影響 使用示例:
前端
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> function web_Socket(url, params) { return new Promise((resolve) => { const socket = new WebSocket(url) socket.onopen = () => { socket.send(JSON.stringify(params)) } socket.onmessage = (res) => { resolve(res.data) } }) } web_Socket('ws://localhost:3000', {}).then(data => { console.log(data); }) </script> </body> </html>
後端
const WebSocket = require('ws') const ws = new WebSocket.Server({ port: 3000 }) ws.on('connection', (obj) => { obj.on('message', (data) => { obj.send('this is a message from WebSocket') }) })
最後就是代理伺服器了
代理伺服器
簡單來講就是客戶端發請求給代理伺服器,代理伺服器發請求給目標伺服器,代理伺服器接收到目標伺服器的響應後,再將響應返回給客戶端。這個過程使得客戶端與目標伺服器之間的直接通訊變成了客戶端與代理伺服器之間的通訊,從而繞過了瀏覽器的同源策略限制。
代理伺服器解決跨域的步驟
客戶端配置代理伺服器:客戶端(通常是前端應用)配置代理伺服器,將原本需要傳送到目標伺服器的請求,改為傳送到本地執行的代理伺服器。
代理伺服器接收請求:代理伺服器接收到客戶端傳送的請求後,解析請求資訊。
代理伺服器轉發請求:代理伺服器將請求轉發到目標伺服器。在此過程中,代理伺服器可以修改請求頭,例如新增或修改 Origin 欄位,以適應目標伺服器的要求。
目標伺服器響應:目標伺服器處理請求,並返回響應資料。
代理伺服器接收響應:代理伺服器接收到目標伺服器的響應後,可能需要對響應頭進行處理,例如去除某些響應頭欄位或新增新的響應頭欄位。
代理伺服器返回響應:代理伺服器將處理後的響應返回給客戶端。
有關跨域的分享就到這裏了