可能是最好的跨域解決方案了

作者 | campcc
今天我們來(lái)聊一個(gè)老生常談的話題,跨域!又是跨域,煩不煩 ?網(wǎng)上跨域的文章那么多,跨的我眼睛都疲勞了,不看了不看了 ?? 別走...我盡量用最簡(jiǎn)單的方式將常見(jiàn)的幾種跨域解決方案給大家闡釋清楚,相信認(rèn)真看完本文,以后不管是作為受試者還是面試官,對(duì)于這塊的知識(shí)都能夠游刃有余。
什么是“跨源”
不是講跨域嗎 ?怎么又來(lái)個(gè)“跨源” ?字都能打錯(cuò)的 ???...稍安勿躁,其實(shí)我們平常說(shuō)的跨域是一種狹義的請(qǐng)求場(chǎng)景,簡(jiǎn)單來(lái)說(shuō),就是“跨“過(guò)瀏覽器的同源策略[1]去請(qǐng)求資“源”,所以我們叫它“跨源”也沒(méi)啥問(wèn)題。那么,跨源,源是什么?瀏覽器的同源策略什么是同源?協(xié)議,域名,端口都相同就是同源干巴巴的,能不能舉個(gè)栗子?栗子:),有的有的:
const url = 'https://www.google.com:3000'
比如上面的這個(gè) URL,協(xié)議是:https,域名是 www.google.com,端口是[2] 3000。不同源了會(huì)怎么樣?會(huì)有很多限制,比如:
Cookie,LocalStorage,IndexDB 等存儲(chǔ)性內(nèi)容無(wú)法讀取 DOM 節(jié)點(diǎn)無(wú)法訪問(wèn) Ajax 請(qǐng)求發(fā)出去了,但是響應(yīng)被瀏覽器攔截了
我就想請(qǐng)求個(gè)東西,至于嗎,為什么要搞個(gè)這么個(gè)東西限制我?基于安全考慮,沒(méi)有它,你可能會(huì)遇到:
Cookie劫持,被惡意網(wǎng)站竊取數(shù)據(jù) 更容易受到 XSS,CSRF 攻擊 無(wú)法隔離潛在惡意文件 ... ...
如果您正在學(xué)習(xí)Spring Boot,推薦一個(gè)連載多年還在繼續(xù)更新的免費(fèi)教程:http://blog.didispace.com/spring-boot-learning-2x/
所以,得有。正是因?yàn)闉g覽器同源策略的存在,你的 Ajax 請(qǐng)求有可能在發(fā)出去后就被攔截了,它還會(huì)給你報(bào)個(gè)錯(cuò):
? Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been block by CORS,
policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
這種發(fā)出去拿不到響應(yīng)的感受,就像你在網(wǎng)上沖浪時(shí),被一股神秘的東方力量限制了一樣:

非常難受,所以,我們接下來(lái)就來(lái)看看怎么用科學(xué)的方法上網(wǎng)(啊呸,科學(xué)的方法解決跨域的問(wèn)題)。
JSONP
這玩意兒就是利用了 <script> 標(biāo)簽的 src 屬性沒(méi)有跨域限制的漏洞,讓我們可以得到從其他來(lái)源動(dòng)態(tài)產(chǎn)生的 JSON 數(shù)據(jù)。為什么叫 JSONP ?JSONP 是 JSON with Padding 的縮寫(xiě),額,至于為什么叫這個(gè)名字,我網(wǎng)上找了下也沒(méi)個(gè)標(biāo)準(zhǔn)的解釋,還望評(píng)論區(qū)的各位老哥知道的趕緊告訴我: ) 怎么實(shí)現(xiàn) ?具體實(shí)現(xiàn)思路大致分為以下步驟:
本站的腳本創(chuàng)建一個(gè) 元素,src 地址指向跨域請(qǐng)求數(shù)據(jù)的服務(wù)器 提供一個(gè)回調(diào)函數(shù)來(lái)接受數(shù)據(jù),函數(shù)名可以通過(guò)地址參數(shù)傳遞進(jìn)行約定 服務(wù)器收到請(qǐng)求后,返回一個(gè)包裝了 JSON 數(shù)據(jù)的響應(yīng)字符串,類似這樣:callback({...}) 如果您正在學(xué)習(xí)Spring Boot,推薦一個(gè)連載多年還在繼續(xù)更新的免費(fèi)教程:http://blog.didispace.com/spring-boot-learning-2x/
瀏覽器接受響應(yīng)后就會(huì)去執(zhí)行回調(diào)函數(shù) callback,傳遞解析后的 JSON 對(duì)象作為參數(shù),這樣我們就可以在 callback 里處理數(shù)據(jù)了。實(shí)際開(kāi)發(fā)中,會(huì)遇到回調(diào)函數(shù)名相同的情況,可以簡(jiǎn)單封裝一個(gè) JSONP 函數(shù):
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
// 創(chuàng)建一個(gè)臨時(shí)的 script 標(biāo)簽用于發(fā)起請(qǐng)求
const script = document.createElement('script');
// 將回調(diào)函數(shù)臨時(shí)綁定到 window 對(duì)象,回調(diào)函數(shù)執(zhí)行完成后,移除 script 標(biāo)簽
window[callback] = data => {
resolve(data);
document.body.removeChild(script);
};
// 構(gòu)造 GET 請(qǐng)求參數(shù),key=value&callback=callback
const formatParams = { ...params, callback };
const requestParams = Object.keys(formatParams)
.reduce((acc, cur) => {
return acc.concat([`${cur}=${formatParams[cur]}`]);
}, [])
.join('&');
// 構(gòu)造 GET 請(qǐng)求的 url 地址
const src = `${url}?${requestParams}`;
script.setAttribute('src', src);
document.body.appendChild(script);
});
}
// 調(diào)用時(shí)
jsonp({
url: 'https://xxx.xxx',
params: {...},
callback: 'func',
})
我們用 Promise 封裝了請(qǐng)求,使異步回調(diào)更加優(yōu)雅,但是別看樓上的洋洋灑灑寫(xiě)了一大段,其實(shí)本質(zhì)上就是:
<script src='https://xxx.xxx.xx?key=value&callback=xxx'><script>
想要看例子 ?戳這里[3]JSONP 的優(yōu)點(diǎn)是簡(jiǎn)單而且兼容性很好,但是缺點(diǎn)也很明顯,需要服務(wù)器支持而且只支持 GET 請(qǐng)求,下面我們來(lái)看第二種方案,也是目前主流的跨域解決方案,劃重點(diǎn)!??
CORS
CORS[4](Cross-Origin Resource Sharing)的全稱叫 跨域資源共享,名稱好高大上,別怕,這玩意兒其實(shí)就是一種機(jī)制。瀏覽器不是有同源策略吶,這東西好是好,但是對(duì)于開(kāi)發(fā)人員來(lái)說(shuō)就不怎么友好了,因?yàn)槲覀兛赡芙?jīng)常需要發(fā)起一個(gè) 跨域 HTTP 請(qǐng)求。我們之前說(shuō)過(guò),跨域的請(qǐng)求其實(shí)是發(fā)出去了的,只不過(guò)被瀏覽器給攔截了,因?yàn)椴话踩f(shuō)直白點(diǎn)兒就是,你想要從服務(wù)器哪兒拿個(gè)東西,但是沒(méi)有經(jīng)過(guò)人家允許啊。所以怎么樣才安全 ?服務(wù)器允許了不就安全了,這就是 CORS 實(shí)現(xiàn)的原理:使用額外的 HTTP 頭來(lái)告訴瀏覽器,讓運(yùn)行在某一個(gè) origin 上的 Web 應(yīng)用允許訪問(wèn)來(lái)自不同源服務(wù)器上的指定的資源。
兼容性
目前,所有的主流瀏覽器都支持 CORS,其中,IE 瀏覽器的版本不能低于 10,IE 8 和 9 需要通過(guò) XDomainRequest 來(lái)實(shí)現(xiàn)。完整的兼容性情況 ? 戳這里[5]
實(shí)現(xiàn)原理
如果您正在學(xué)習(xí)Spring Boot,推薦一個(gè)連載多年還在繼續(xù)更新的免費(fèi)教程:http://blog.didispace.com/spring-boot-learning-2x/
CORS 需要瀏覽器和服務(wù)器同時(shí)支持,整個(gè) CORS 的通信過(guò)程,都是瀏覽器自動(dòng)完成。怎么個(gè)自動(dòng)法 ?簡(jiǎn)單來(lái)說(shuō),瀏覽器一旦發(fā)現(xiàn)請(qǐng)求是一個(gè)跨域請(qǐng)求,首先會(huì)判斷請(qǐng)求的類型,如果是簡(jiǎn)單請(qǐng)求,會(huì)在請(qǐng)求頭中增加一個(gè) Origin 字段,表示這次請(qǐng)求是來(lái)自哪一個(gè)源。而服務(wù)器接受到請(qǐng)求后,會(huì)返回一個(gè)響應(yīng),響應(yīng)頭中會(huì)包含一個(gè)叫 Access-Control-Allow-Origin 的字段,它的值要么包含由 Origin 首部字段所指明的域名,要么是一個(gè) "*",表示接受任意域名的請(qǐng)求。如果響應(yīng)頭中沒(méi)有這個(gè)字段,就說(shuō)明當(dāng)前源不在服務(wù)器的許可范圍內(nèi),瀏覽器就會(huì)報(bào)錯(cuò):
GET /cors HTTP/1.1
Origin: https://xxx.xx
Accept-Language: en-US
Connection: keep-alive
... ...
如果是非簡(jiǎn)單請(qǐng)求,會(huì)在正式通信之前,發(fā)送一個(gè)預(yù)檢請(qǐng)求(preflight),目的在于詢問(wèn)服務(wù)器,當(dāng)前網(wǎng)頁(yè)所在的域名是否在服務(wù)器的許可名單之中,以及可以使用哪些 HTTP 動(dòng)詞和頭信息字段,只有得到肯定答復(fù),瀏覽器才會(huì)發(fā)出正式的請(qǐng)求,否則就報(bào)錯(cuò)。你可能發(fā)現(xiàn)我們?cè)谌粘5拈_(kāi)發(fā)中,會(huì)看到很多使用 OPTION 方法發(fā)起的請(qǐng)求,它其實(shí)就是一個(gè)預(yù)檢請(qǐng)求:
OPTIONS /cors HTTP/1.1
Origin: http://xxx.xx
Access-Control-Request-Method: PUT
Accept-Language: en-US
... ...
那么到底哪些是簡(jiǎn)單請(qǐng)求,哪些是非簡(jiǎn)單請(qǐng)求 ?
請(qǐng)求類型
不會(huì)觸發(fā) CORS 預(yù)檢的,就是簡(jiǎn)單請(qǐng)求。哪些請(qǐng)求不會(huì)觸發(fā)預(yù)檢 ?使用以下方法之一:GET, HEAD, POST,并且 Content-Type 的值僅限于下列三者之一:
text/plain multipart/form-data application/x-www-form-urlencoded
相反,不符合上述條件的就是非簡(jiǎn)單請(qǐng)求啦。所以,實(shí)現(xiàn) CORS 的關(guān)鍵是服務(wù)器,只要服務(wù)器實(shí)現(xiàn)了 CORS 的相關(guān)接口,就可以實(shí)現(xiàn)跨域。CORS 與 JSONP相比,優(yōu)勢(shì)是支持所有的請(qǐng)求方法,缺點(diǎn)是兼容性上較 JSONP 差。除了 JSONP 和 CORS 外,還有一種常用的跨域解決方案:PostMessage,它更多地用于窗口間的消息傳遞。
PostMessage
PostMessage 是 Html5 XMLHttpRequest Level 2 中的 API,它可以實(shí)現(xiàn)跨文檔通信(Cross-document messaging)。兼容性上,IE8+,Chrome,F(xiàn)irfox 等主流瀏覽器都支持,可以放心用??,如何理解跨文檔通信?你可以類比設(shè)計(jì)模式中的發(fā)布-訂閱模式,在這里,一個(gè)窗口發(fā)送消息,另一個(gè)窗口接受消息,之所以說(shuō)類似發(fā)布-訂閱模式,而不是觀察者模式,是因?yàn)檫@里兩個(gè)窗口間沒(méi)有直接通信,而是通過(guò)瀏覽器這個(gè)第三方平臺(tái)。
window.postMessage(message, origin, [transfer])
postMessage 方法接收三個(gè)參數(shù),要發(fā)送的消息,接收消息的源和一個(gè)可選的 Transferable 對(duì)象,如何接收消息 ?
window.addEventListener("message", function receiveMessage(event) {}, false); // 推薦,兼容性更好
window.onmessage = function receiveMessage(event) {} // 不推薦,這是一個(gè)實(shí)驗(yàn)性的功能,兼容性不如上面的方法
接收到消息后,消息對(duì)象 event 中包含了三個(gè)屬性:source,origin,data,其中 data 就是我們發(fā)送的 message。此外,除了實(shí)現(xiàn)窗口通信,postMessage 還可以同 Web Worker 和 Service Work 進(jìn)行通信,有興趣的可以 戳這里[6]。
Websocket
Websocket 是 HTML5 的一個(gè)持久化的協(xié)議,它實(shí)現(xiàn)了瀏覽器與服務(wù)器的全雙工通信,同時(shí)也是跨域的一種解決方案。什么是全雙工通信 ?簡(jiǎn)單來(lái)說(shuō),就是在建立連接之后,server 與 client 都能主動(dòng)向?qū)Ψ桨l(fā)送或接收數(shù)據(jù)。原生的 WebSocket API 使用起來(lái)不太方便,我們一般會(huì)選擇自己封裝一個(gè) Websocket(嗯,我們團(tuán)隊(duì)也自己封了一個(gè) : ))或者使用已有的第三方庫(kù),我們這里以第三方庫(kù) ws[7] 為例:
const WebSocket = require('ws');
const ws = new WebSocket('ws://www.host.com/path');
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function incoming(data) {
console.log(data);
});
... ...
需要注意的是,Websocket 屬于長(zhǎng)連接,在一個(gè)頁(yè)面建立多個(gè) Websocket 連接可能會(huì)導(dǎo)致性能問(wèn)題。
如果您正在學(xué)習(xí)Spring Boot,推薦一個(gè)連載多年還在繼續(xù)更新的免費(fèi)教程:http://blog.didispace.com/spring-boot-learning-2x/
Nginx 反向代理
我們知道同源策略限制的是:瀏覽器向服務(wù)器發(fā)送跨域請(qǐng)求需要遵循的標(biāo)準(zhǔn),那如果是服務(wù)器向服務(wù)器發(fā)送跨域請(qǐng)求呢?答案當(dāng)然是,不受瀏覽器的同源策略限制。利用這個(gè)思路,我們就可以搭建一個(gè)代理服務(wù)器,接受客戶端請(qǐng)求,然后將請(qǐng)求轉(zhuǎn)發(fā)給服務(wù)器,拿到響應(yīng)后,再將響應(yīng)轉(zhuǎn)發(fā)給客戶端:

這就是 Nginx 反向代理的原理,只需要簡(jiǎn)單配置就可以實(shí)現(xiàn)跨域:
# nginx.config
# ...
server {
listen 80;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
index index.html index.htm;
# 當(dāng)用 webpack-dev-server 等中間件代理接口訪問(wèn) nignx 時(shí),此時(shí)無(wú)瀏覽器參與,故沒(méi)有同源限制,下面的跨域配置可不啟用
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Credentials true;
# ...
}
}
Node 中間件代理
另外,如果您正在學(xué)習(xí)Spring Cloud,推薦一個(gè)連載多年還在繼續(xù)更新的免費(fèi)教程:https://blog.didispace.com/spring-cloud-learning/
實(shí)現(xiàn)的原理和我們前文提到的代理服務(wù)器原理如出一轍,只不過(guò)這里使用了 Node 中間件做為代理。需要注意的是,瀏覽器向代理服務(wù)器請(qǐng)求時(shí)仍然遵循同源策略,別忘了在 Node 層通過(guò) CORS 做跨域處理:
const https = require('https')
// 接受客戶端請(qǐng)求
const sever = https.createServer((req, res) => {
...
const { method, headers } = req
// 設(shè)置 CORS 允許跨域
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type',
...
})
// 請(qǐng)求服務(wù)器
const proxy = https.request({ host: 'xxx', method, headers, ...}, response => {
let body = ''
response.on('data', chunk => { body = body + chunk })
response.on('end', () => {
// 響應(yīng)結(jié)果轉(zhuǎn)發(fā)給客戶端
res.end(body)
})
})
// 結(jié)束請(qǐng)求
proxy.end()
})
document.domain
另外,如果您正在學(xué)習(xí)Spring Cloud,推薦一個(gè)連載多年還在繼續(xù)更新的免費(fèi)教程:https://blog.didispace.com/spring-cloud-learning/
二級(jí)域名相同的情況下,設(shè)置 document.domain 就可以實(shí)現(xiàn)跨域。什么是二級(jí)域名 ?a.test.com 和 b.test.com 就屬于二級(jí)域名,它們都是 test.com 的子域。如何實(shí)現(xiàn)跨域 ?
document.domain = 'test.com' // 設(shè)置 domain 相同
// 通過(guò) iframe 嵌入跨域的頁(yè)面
const iframe = document.createElement('iframe')
iframe.setAttribute('src', 'b.test.com/xxx.html')
iframe.onload = function() {
// 拿到 iframe 實(shí)例后就可以直接訪問(wèn) iframe 中的數(shù)據(jù)
console.log(iframe.contentWindow.xxx)
}
document.appendChild(iframe)
總結(jié)
當(dāng)然,除了上述的方案外,比較 Hack 的還有:window.name, location.hash,但是這些跨域的方式現(xiàn)在我們已經(jīng)不推薦了,為什么 ?因?yàn)?strong style="color: rgb(53, 179, 120);">相比之下有更加安全和強(qiáng)大的 PostMessage 作為替代。跨域的方案其實(shí)有很多,總結(jié)下來(lái):
CORS 支持所有的 HTTP 請(qǐng)求,是跨域最主流的方案 JSONP 只支持 GET 請(qǐng)求,但是可以兼容老式瀏覽器 Node 中間件和 Nginx 反向代理都是利用了服務(wù)器對(duì)服務(wù)器沒(méi)有同源策略限制 Websocket 也是一種跨域的解決方案 PostMessage 可以實(shí)現(xiàn)跨文檔通信,更多地用于窗口通信 document.domain, window.name, location.hash 逐漸淡出歷史舞臺(tái),作為替代 PostMessage 是一種不錯(cuò)的方案
寫(xiě)在最后
本文首發(fā)于我的 博客[8],才疏學(xué)淺,難免有錯(cuò)誤,文章有誤之處還望不吝指正!如果有疑問(wèn)或者發(fā)現(xiàn)錯(cuò)誤,可以在相應(yīng)的 issues 進(jìn)行提問(wèn)或勘誤如果喜歡或者有所啟發(fā),歡迎 star,對(duì)作者也是一種鼓勵(lì)(完)
參考資料
[1]https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy [2]http://www.google.com**%EF%BC%8C%E7%AB%AF%E5%8F%A3%E6%98%AF [3]https://codepen.io/lycheelee/pen/MWWXKeN?editors=1000 [4]https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS [5]https://caniuse.com/#search=CORS [6]https://juejin.cn/post/6844903665694687240#heading-9 [7]https://github.com/websockets/ws [8]https://github.com/campcc/blog
往期推薦
技術(shù)交流群
最近有很多人問(wèn),有沒(méi)有讀者交流群,想知道怎么加入。加入方式很簡(jiǎn)單,有興趣的同學(xué),只需要點(diǎn)擊下方卡片,回復(fù)“加群“,即可免費(fèi)加入我們的高質(zhì)量技術(shù)交流群!
點(diǎn)擊閱讀原文,送你免費(fèi)Spring Boot教程!
