什么是跨域艺沼?
瀏覽器同源策略: 即要求“協(xié)議、域名骚露、端口”必須相同蹬挤,同源策略是瀏覽器的一個(gè)安全功能,不同源的客戶端腳本在沒有明確授權(quán)的情況下棘幸,不能讀寫對方資源
只要通信中協(xié)議焰扳、域名、端口中有任意一個(gè)不同误续,就稱之為跨域吨悍。同源限制是瀏覽器的行為,實(shí)際上雙方通信是通的蹋嵌,但瀏覽器會攔截讓客戶端收不到服務(wù)器返回的信息育瓜。
一般跨域會在瀏覽器的console日志中會提示錯(cuò)誤:
Access to XMLHttpRequest at 'http://localhost:4000/getData' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
還有可能在Network請求中提示錯(cuò)誤:
Name | status |
---|---|
getData | CORS error |
如我們常見的ajax請求,就不支持跨域
請求跨域解決方案
jsonp
再說jsonp之前栽烂,我們先了解下不受跨域影響的標(biāo)簽躏仇,簡單來說,就是帶src的標(biāo)簽腺办,如img, script等焰手,如下例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- script引入不同源的vue文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="http://localhost:4000/getData"></script>
<title>Document</title>
</head>
<body>
<!-- img標(biāo)簽 -->
<img src="https://ss0.bdstatic.com/70cFvHSh_Q1YnxGkpoWK1HF6hhy/it/u=2582512942,3155345292&fm=26&gp=0.jpg" alt="">
<script>
console.log(Vue) // 可以直接執(zhí)行引入的script腳本
console.log(a) // 20
</script>
</body>
</html>
// 服務(wù)器:http://localhost:4000
router.get('/getData', ctx => {
ctx.body = "var a=20;"
})
而jsonp,就是利用了script標(biāo)簽引用js文件不受同源策略影響的原理怀喉,通過動態(tài)創(chuàng)建script標(biāo)簽來實(shí)現(xiàn)的书妻。實(shí)現(xiàn)做法是:
- 客戶端動態(tài)創(chuàng)建一個(gè)script標(biāo)簽,給其添加src屬性躬拢,寫上跨域url驻子,并創(chuàng)建一個(gè)回調(diào)函數(shù)的querystring,比如定義為callback=myfunction估灿, 并將該script標(biāo)簽添加到body上元素上
- 定義要執(zhí)行的回調(diào)函數(shù)myfunction
- 服務(wù)器在接收到請求后崇呵,拿到對應(yīng)資源后,通過鍵名callback拿到前端傳遞的方法名馅袁,并返回一個(gè)該回調(diào)函數(shù)執(zhí)行的指令給客戶端(對服務(wù)器來說域慷,執(zhí)行函數(shù)的指令是個(gè)字符串,所以不會執(zhí)行)
- 客戶端拿到服務(wù)器的應(yīng)答時(shí)汗销,就會執(zhí)行這個(gè)回調(diào)函數(shù)犹褒,從而獲取對應(yīng)的資源
缺點(diǎn):
- 只支持get請求,限制了參數(shù)大小和類型
- 請求過程無法終止弛针,導(dǎo)致弱網(wǎng)絡(luò)下處理超時(shí)請求比較麻煩
- 無法捕獲服務(wù)端返回的異常信息
<!-- 客戶端 -->
<body>
<button onclick="sendJsonp()">通過jsonp解決跨域</button>
<script>
// 1. JSONP
function sendJsonp() {
const script = document.createElement('script')
// 通過querystring的方式叠骑,傳遞一個(gè)回調(diào)函數(shù)的參數(shù),這個(gè)回調(diào)參數(shù)的鍵值是前后端一起定義的并保持一致的
script.src = "http://localhost:4000/jsonpData?callback=customFunc"
document.body.appendChild(script)
}
// 自定義的函數(shù)名削茁,即使用jsonp傳遞給后臺的回調(diào)函數(shù)名
function customFunc(res) {
console.log(res) // 1. 200; 2. {name: "hahha", age: 3}
}
</script>
</body>
// 服務(wù)器端
const Koa = require("koa")
const Router = require("koa-router")
const app = new Koa()
const router = new Router()
router.get('/jsonpData', ctx => {
const callback = ctx.query.callback
// ctx.body = `${callback}(200)` // 后端拿到前端傳遞過來的函數(shù)名后宙枷,返回一個(gè)函數(shù)執(zhí)行的指令給前端掉房,前端拿到后會立即執(zhí)行該函數(shù)
// 如果參數(shù)是一個(gè)對象,那要將其轉(zhuǎn)換成字符串
let obj = {
name: 'hahha',
age: 3
}
let objStr = JSON.stringify(obj)
ctx.body = `${callback}(${objStr})` // 注意慰丛,如果參數(shù)直接傳objStr卓囚,客戶端會認(rèn)為這是一個(gè)變量,會報(bào)未找到異常
})
app.use(router.routes())
app.listen(4000)
CORS解決跨域
CORS(cross-origin resource sharing)诅病,跨域資料共享哪亿,是瀏覽器為AJAX請求設(shè)置的一種跨域機(jī)制,讓其可以在服務(wù)端允許的情況下進(jìn)行跨域訪問贤笆。它比jsonp更加優(yōu)雅蝇棉。
它主要是通過設(shè)置http響應(yīng)頭來告訴瀏覽器,服務(wù)端是否允許當(dāng)前域的腳本進(jìn)行跨域訪問芥永。
跨域資源共享將AJAX請求分為了兩類:簡單請求和復(fù)雜請求篡殷。
簡單請求
符合以下兩個(gè)特征:
- 請求方法為head、get恤左、post
- 請求頭只接受以下字段:
- Accept:瀏覽器能夠接受的響應(yīng)內(nèi)容類型
- Accept-Language: 瀏覽器能接受的自然語言列表
- Content-Type: 請求對應(yīng)的類型,只能為以下三種:
1)text/plain- multipart/form-data
- application/x-www-form-urlencoded
- Content-Language:瀏覽器希望采用的自然語言
- Save-Data:瀏覽器是否希望減少數(shù)據(jù)傳輸量
對于簡單請求:
- 瀏覽器發(fā)出簡單請求時(shí)搀绣,會在請求頭增加一個(gè)origin字段飞袋,值為請求源的信息;
- 服務(wù)器收到請求后链患,根據(jù)請求頭origin判斷巧鸭,返回相應(yīng)的內(nèi)容
- 瀏覽器收到響應(yīng)后,根據(jù)響應(yīng)頭Access-Control-Allow-Origin進(jìn)行判斷麻捻,這個(gè)字段是服務(wù)端允許跨域請求的源纲仍,如果響應(yīng)頭沒有包含這個(gè)字段或者這個(gè)響應(yīng)頭中的值沒有包含當(dāng)前源,則會拋出錯(cuò)誤贸毕;如果有郑叠,則是允許當(dāng)前源進(jìn)行跨域請求。
復(fù)雜請求
只要不滿足簡單請求特征中的任意一條明棍,就屬于復(fù)雜請求
對于復(fù)雜請求:
- 會預(yù)先發(fā)個(gè)
options
預(yù)檢請求乡革,瀏覽器會在請求頭添加Access-control-Request-Method
字段,值為跨域請求的請求方法摊腋,用于探查目標(biāo)接口沸版,允許那些請求方式; - 如果添加了不屬性于簡單請求的頭部字段兴蒸,瀏覽器還會添加一個(gè)
Access-Control-Request-Headers
字段视粮,值為跨域請求添加的請求頭部字段 - 服務(wù)器接收到請求后,除了會返回
Access-Control-Allow-Origin
的字段外,還會根據(jù)請求頭橙凳,返回對應(yīng)的響應(yīng)頭Access-control-Request-Methods
和Access-Control-Allow-Headers
蕾殴,告訴瀏覽器服務(wù)端允許的源笑撞、方法和請求頭字段,并返回 204 狀態(tài)碼区宇。 - 瀏覽器得到預(yù)檢請求的響應(yīng)后娃殖,會判斷當(dāng)前請求是否在服務(wù)端的許可范圍內(nèi),如果在议谷,則繼續(xù)發(fā)送跨域請求炉爆;否則,則直接報(bào)錯(cuò)
Websocket
Websocket 是 HTML5 規(guī)范提出的一個(gè)應(yīng)用層的全雙工協(xié)議卧晓,適用于瀏覽器與服務(wù)器進(jìn)行實(shí)時(shí)通信場景芬首。
什么叫全雙工呢?
這是通信傳輸?shù)囊粋€(gè)術(shù)語逼裆,這里的“工”指的是通信方向郁稍,“雙工”是指從客戶端到服務(wù)端,以及從服務(wù)端到客戶端兩個(gè)方向都可以通信胜宇,“全”指的是通信雙方可以同時(shí)向?qū)Ψ桨l(fā)送數(shù)據(jù)耀怜。與之相對應(yīng)的還有半雙工和單工,半雙工指的是雙方可以互相向?qū)Ψ桨l(fā)送數(shù)據(jù)桐愉,但雙方不能同時(shí)發(fā)送财破,單工則指的是數(shù)據(jù)只能從一方發(fā)送到另一方。
WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單从诲,允許服務(wù)端主動向客戶端推送數(shù)據(jù)左痢。在 WebSocket API 中,瀏覽器和服務(wù)器只需要完成一次握手系洛,兩者之間就直接可以創(chuàng)建持久性的連接俊性,并進(jìn)行雙向數(shù)據(jù)傳輸。
Websocket的實(shí)現(xiàn):
一個(gè)網(wǎng)頁創(chuàng)建一個(gè)WebSocket連接,連接到另一個(gè)網(wǎng)頁(或服務(wù)器),然后調(diào)用send()方法向另一個(gè)網(wǎng)頁發(fā)送消息减宣,通過監(jiān)聽onmessage事件得到另一個(gè)網(wǎng)頁發(fā)送的消息适袜。
if ("WebSocket" in window) {
// 創(chuàng)建一個(gè)連接另一個(gè)網(wǎng)頁的ws實(shí)例
var ws = new WebSocket("ws://b.com");
// 連接建立時(shí)觸發(fā)的事件
ws.onopen = function(){
// 發(fā)送消息
ws.send(...);
}
ws.onmessage = function(e){
// 接收消息
console.log(e.data);
}
// 關(guān)閉連接
ws.close()
}else {
alert("您的瀏覽器不支持 WebSocket!");
}
代理轉(zhuǎn)發(fā)
既然同源策略是瀏覽器設(shè)置的安全策略,那么,我們只要不通過瀏覽器直接發(fā)送請求,而是通過服務(wù)器來發(fā)送請求,那么就不存在同源限制了宫峦。
所以我們可以把這個(gè)模式轉(zhuǎn)換下:
瀏覽器 -> 不同源服務(wù)器 發(fā)送請求
改為:
瀏覽器 -> 同源服務(wù)器 -> 不同源服務(wù)器 發(fā)請求
這就是我們說的代理轉(zhuǎn)發(fā)的原理。
在客戶端使用的代理稱為“正向代理”玫鸟,在服務(wù)端設(shè)置的代理叫做“反向代理”导绷。代理轉(zhuǎn)發(fā)實(shí)現(xiàn)起來非常簡單,在當(dāng)前被訪問的服務(wù)器配置一個(gè)請求轉(zhuǎn)發(fā)規(guī)則就行了屎飘。
// 正向代理
// webpack.config.js
module.exports = {
//...
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
};
在 Nginx 服務(wù)器上配置同樣的轉(zhuǎn)發(fā)規(guī)則也非常簡單妥曲,下面是示例配置(反向代理)贾费。
通過 location 指令匹配路徑,然后通過 proxy_pass 指令指向代理地址即可檐盟。
location /api {
proxy_pass http://localhost:3000;
}
頁面跨域解決方案
除了瀏覽器請求跨域之外褂萧,頁面之間也會有跨域需求,例如使用 iframe 時(shí)父子頁面之間進(jìn)行通信葵萎。
postMessage
HTML5 推出了一個(gè)新的函數(shù) postMessage() 用來實(shí)現(xiàn)父子頁面之間通信导犹,而且不論這兩個(gè)頁面是否同源。
實(shí)現(xiàn)羡忘,父頁面向子頁面發(fā)消息
// http://www.fahter.com
// 父頁面打開子頁面
let son = window.open('http://www.son.com')
// 父頁面向子頁面發(fā)消息
son.postMessage('I am your father', 'http://www.son.com');
// http://www.son.com
// 子頁面通過監(jiān)聽message獲取父頁面的消息
window.addEventListener('message', function(e) {
console.log(e.data);
},false);
// 子頁面通過window.opener.postMessage給父頁面發(fā)消息
window.opener.postMessage('I am your son', 'http://www.fahter.com');
修改域名document.domain
由于JavaScript同源策略的限制谎痢,腳本只能讀取和所屬文檔來源相同的窗口和文檔的屬性。
對于已經(jīng)有成熟產(chǎn)品體系的公司來說卷雕,不同的頁面可能放在不同的服務(wù)器上节猿,這些服務(wù)器域名不同,但是擁有相同的上級域名漫雕,比如id.qq.com滨嘱、www.qq.com、user.qzone.qq.com浸间,它們都有公共的上級域名qq.com太雨。這些服務(wù)器上的頁面之間的跨域訪問可以通過document.domain來進(jìn)行。
默認(rèn)情況下发框,document.domain存放的是載入文檔的服務(wù)器的主機(jī)名躺彬,可以手動設(shè)置這個(gè)屬性煤墙,不過是有限制的梅惯,只能設(shè)置成當(dāng)前域名或者上級的域名,并且必須要包含一個(gè).號仿野,也就是說不能直接設(shè)置成頂級域名铣减。例如:id.qq.com,可以設(shè)置成qq.com脚作,但是不能設(shè)置成com葫哗。
具有相同document.domain的頁面,就相當(dāng)于是處在同域名的服務(wù)器上球涛,如果協(xié)議和端口號也是一致劣针,那它們之間就可以跨域訪問數(shù)據(jù)。