跨域請(qǐng)求
全稱:非同源策略請(qǐng)求(同源即同協(xié)議、同域名和同端口)曲稼,而ajax專門用于處理同源策略請(qǐng)求索绪,因此對(duì)于非同源請(qǐng)求,特別是在前后端分離的項(xiàng)目當(dāng)中贫悄,我們就會(huì)面臨這類問(wèn)題瑞驱。
這里要聲明一下:跨域是瀏覽器行為,而不是服務(wù)器行為(當(dāng)前臺(tái)向服務(wù)器發(fā)起請(qǐng)求時(shí)清女,后臺(tái)并沒(méi)有阻止請(qǐng)求操作钱烟,但后臺(tái)返回?cái)?shù)據(jù)時(shí),瀏覽器基于安全考慮嫡丙,若非同源請(qǐng)求拴袭,將可能禁止對(duì)請(qǐng)求數(shù)據(jù)的處理)
常用跨域解決方案
基于Jsonp
我們可以發(fā)現(xiàn)在html當(dāng)中含有src
屬性的標(biāo)簽,如<srcipt>
/<img>
/<link>
/<iframe>
標(biāo)簽曙博,在請(qǐng)求資源時(shí)都不存在跨域請(qǐng)求的限制拥刻。而jsonp則是基于<script>
標(biāo)簽去請(qǐng)求資源,服務(wù)器則在返回?cái)?shù)據(jù)資源時(shí)將其包在一個(gè)本地可調(diào)用的全局函數(shù)里返回父泳,然后本地則調(diào)用這個(gè)函數(shù)
原理詳解
首先因由于同源策略般哼,我們無(wú)法通過(guò)ajax請(qǐng)求來(lái)獲取不符合條件的資源,但是假如我們用<script>
標(biāo)簽請(qǐng)求的js文件中有這樣的語(yǔ)句:
test({"x": 1})
而我們前臺(tái)的請(qǐng)求如下:
<script src="http://xxx.xxx.com/xxx.js"></script>
那么獲取到請(qǐng)求以后就會(huì)執(zhí)行test
這個(gè)函數(shù)惠窄,并將一個(gè)對(duì)象作為參數(shù)傳入蒸眠,此時(shí)如果我們的前臺(tái)本身有test
這個(gè)函數(shù),那么就可以成功執(zhí)行這段代碼杆融。換個(gè)說(shuō)法楞卡,如果現(xiàn)在我們通過(guò)訪問(wèn)一個(gè)后臺(tái)接口獲取到跟前面一樣的字符串(之前是通過(guò)訪問(wèn)一個(gè)遠(yuǎn)程js資源得到的),那么因?yàn)槎际怯?code><script>標(biāo)簽獲取的,獲取的結(jié)果也一樣蒋腮,只是請(qǐng)求的url稍微有些改變淘捡,可以發(fā)現(xiàn)結(jié)果都是是一樣的:正常執(zhí)行test
這個(gè)函數(shù),例如下面這段代碼:
<script src="http://xxx.xxx.com/api"></script>
<script>
function test(data) {
console.log(data);
}
</script>
那么控制臺(tái)就會(huì)成功輸出data的內(nèi)容池摧。這就是jsonp的原理焦除,可以看出和ajax請(qǐng)求的本質(zhì)是完全不一樣的,ajax是基于XmlHttpRequest作彤,而jsonp則是基于標(biāo)簽動(dòng)態(tài)請(qǐng)求膘魄,可以說(shuō)是一種偽請(qǐng)求
缺點(diǎn):由于是基于標(biāo)簽的src屬性請(qǐng)求的,只能使用get請(qǐng)求宦棺、并且安全性不好
基于CORS跨域資源共享配置
CORS是十分常用的一種跨域解決方案瓣距,其只需要在服務(wù)端配置對(duì)應(yīng)的響應(yīng)頭屬性即可,而無(wú)需前端做任何操作代咸,最關(guān)鍵的就是在返回頭里配置Access-Control-Allow-Origin
屬性蹈丸,舉例(這里基于flask進(jìn)行示例):
from flask import Flask, make_response
import json
app = Flask(__name__)
@app.route('/test', methods=['GET'])
def user_info():
response = make_response(json.dumps({'data': 'content'}))
response.headers['Access-Control-Allow-Origin'] = '*'
return response
if __name__ == '__main__':
app.run(debug=True, port=5000)
還有一些其他訪問(wèn)控制的配置如下:
Access-Control-Allow-Methods: POST, GET, PUT, DELETE, HEAD, OPTIONS
# 允許的請(qǐng)求方式
#(注意只有POST, GET以及HEAD是默認(rèn)允許的請(qǐng)求,其他的請(qǐng)求都必須先發(fā)送一個(gè)OPTIONS的預(yù)請(qǐng)求
# 當(dāng)預(yù)請(qǐng)求得到認(rèn)可后才可以進(jìn)行請(qǐng)求呐芥,而只有在該配置當(dāng)中配置的方法才會(huì)得到認(rèn)可)
Access-Control-Allow-Headers: 'Content-Type, ...'
# 允許請(qǐng)求的頭部信息逻杖,沒(méi)有設(shè)置在里面的header都是不允許的
Access-Control-Max-Age: '1000'
# 設(shè)置允許跨域的時(shí)間,此時(shí)在有效期內(nèi)無(wú)需再發(fā)送預(yù)請(qǐng)求進(jìn)行驗(yàn)證思瘟,直接發(fā)請(qǐng)求就可以了荸百,單位是s
Access-Control-Allow-Credentials: true
# 是否允許發(fā)送cookie
詳細(xì)參考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
缺點(diǎn):cors配置源地址只能配置*
(多源)或者一個(gè)地址,并且如果配置的是*
滨攻,那么基于安全性問(wèn)題够话,請(qǐng)求時(shí)將無(wú)法攜帶cookie
注:
如果不想配置成*
,又希望多個(gè)地址可以光绕,那么可以在程序當(dāng)中判斷訪問(wèn)的地址是否是允許的女嘲,如果是,則加入到cors配置當(dāng)中
基于Http Proxy
由于cors的弊端诞帐,又出了Http Proxy的方案欣尼,需要在webpack中配置,常在基于vue停蕉、react的前后端分離項(xiàng)目中使用愕鼓,首先要安裝對(duì)應(yīng)模塊包:
npm install webpack webpack-dev-server
然后在webpack的配置文件webpack.config.js
中配置:
devServer: {
port: 8080,
proxy: {
'/': {
target: 'http://127.0.0.1:6666',
// 代理的地址
// secure: false,
// 如果是https接口,需要配置這個(gè)參數(shù)
changeOrigin: true
// 是否跨域
}
}
}
vue-cli中配置
如果是基于vue-cli的項(xiàng)目工程慧起,那么這里介紹兩種方式實(shí)現(xiàn)proxy
代理的配置:
- 第一種:配置
devServer
菇晃,在build/webpack.base.conf.js
中的module.exports
進(jìn)行和上面相同的配置 - 第二種:配置
proxyTable
,直接在config/index.js
中的module.exports.dev
配置如下:
module.exports = {
dev: {
...
proxyTable: {
// 代理配置
'/': {
target: 'http://127.0.0.1:6666',
changeOrigin: true,
// pathRewrite: {
// '^/apis': '/api' //重寫(xiě)的路徑
// }
}
},
},
...
}
注:
配置proxy之后蚓挤,實(shí)際上前端請(qǐng)求還是發(fā)給本身的node服務(wù)器(查看網(wǎng)絡(luò)請(qǐng)求就可以發(fā)現(xiàn))磺送,例如前端是8080
剩失,配置了6666
的代理,那么前端會(huì)先請(qǐng)求本身册着,即8080
,然后8080
再通過(guò)node去訪問(wèn)6666
脾歧,由于服務(wù)器之間的請(qǐng)求不存在跨域問(wèn)題甲捏,所以6666
返回?cái)?shù)據(jù)給node,node再返回給前端鞭执,由于同源司顿,所以此時(shí)數(shù)據(jù)也就沒(méi)有跨域問(wèn)題了
注:
在vue-cli3.0+以后,則直接在項(xiàng)目根目錄下創(chuàng)建文件vue.config.js
兄纺,并添加如下內(nèi)容即可:
module.exports = {
devServer: {
proxy: {
"/": {
target: "http://127.0.0.1:6666/",
changeOrigin: true
}
}
}
};
基于nginx配置CORS
在nginx中也可以配置cors
大溜,舉例:
server {
listen 80;
server_name localhost;
...
location / {
root html;
index index.html index.htm;
proxy_pass http://xxx;
add_header Access-Control-Allow-Origin *; # 配置cors
...
}
基于nginx配置反向代理
假如前臺(tái)服務(wù)端口為http://127.0.0.1:3000
,后臺(tái)服務(wù)端接口為http://127.0.0.1:8000/api
估脆,那么因?yàn)椴煌辞辗埽厝淮嬖诳缬騿?wèn)題,此時(shí)可以在nginx中進(jìn)行配置如下:監(jiān)聽(tīng)3000
端口疙赠,并配置/api
的反向代理地址為:http://127.0.0.1:8000/api
付材,配置文件示例如下:
server {
listen 3000;
server_name localhost;
...
location /api/{
...
proxy_pass http://127.0.0.1:8000/api/; # 配置反向代理
}
}
再將前臺(tái)請(qǐng)求的接口改為:http://127.0.0.1:3000/api
,即可解決跨域問(wèn)題圃阳。
(原理:經(jīng)過(guò)nginx的反向代理配置厌衔,現(xiàn)在訪問(wèn)http://127.0.0.1:3000/api
就相當(dāng)于訪問(wèn)http://127.0.0.1:8000/api
,而前臺(tái)請(qǐng)求的url因?yàn)楦某闪?code>http://127.0.0.1:3000/api捍岳,在瀏覽器看來(lái)前臺(tái)和請(qǐng)求接口同源富寿,也就不存在跨域問(wèn)題了)
其他跨域解決方案
基于修改本地host文件(不推薦)
例如前臺(tái)地址:127.0.0.1:6666
,而后臺(tái)接口地址:http://api.xxx.com
锣夹,此時(shí)如果想要訪問(wèn)后臺(tái)接口页徐,可以在本地host文件中加一行:
127.0.0.1:6666 http://api.xxx.com
但這種方式實(shí)際上只是在模仿同源請(qǐng)求,并沒(méi)有實(shí)質(zhì)地解決跨域問(wèn)題
基于Iframe的postMessage
postMessage方法允許頁(yè)面間基于Iframe進(jìn)行消息傳遞晕城,例如A和B頁(yè)面進(jìn)行消息傳遞:
- a.html
<html>
<body>
<iframe
id="iframe"
src="http://127.0.0.1:5500/b.html"
style="display: none;"
></iframe>
</body>
<script>
iframe.onload = function() {
iframe.contentWindow.postMessage("來(lái)自A的信息...", "http://127.0.0.1:5500/b.html");
// 發(fā)送消息給頁(yè)面B
};
// 監(jiān)聽(tīng)頁(yè)面B發(fā)來(lái)的消息
window.onmessage = function(ev) {
console.log("A收到B的信息:", ev.data);
};
</script>
</html>
- b.html
<html>
<body></body>
<script>
// 監(jiān)聽(tīng)頁(yè)面A發(fā)來(lái)的消息
window.onmessage = function(ev) {
console.log("B收到A的信息:", ev.data);
ev.source.postMessage("回信...", "*");
};
</script>
</html>
基于H5的web socket
由于原生web socket
不太好使用泞坦,這里使用socket.io
(對(duì)web socket
進(jìn)行了封裝的框架)進(jìn)行示例:
- 服務(wù)端(基于node):
const server = require("http").createServer();
const io = require("socket.io")(server);
io.on("connection", client => {
client.on("event", data => {
console.log(data);
});
client.on("message", msg => {
console.log(msg);
});
client.on("disconnect", () => {
console.log("server has closed!");
});
});
server.listen(3000);
- 客戶端:
<html>
<body></body>
<script src="https://cdn.bootcss.com/socket.io/2.3.0/socket.io.js"></script>
<script>
let socket = io('http://127.0.0.1:3000/');
socket.on('connect', () => {
socket.on('message', msg => {
console.log(msg);
})
socket.on('disconnect', () => {
console.log('server has closed!');
})
})
socket.send('test');
</script>
</html>
web socket使用參考:https://zhuanlan.zhihu.com/p/74326818
使用WebSocket進(jìn)行跨域數(shù)據(jù)請(qǐng)求參考:https://blog.csdn.net/itkingone/article/details/83818278