視頻參考:ajax跨域完全講解
本文精華版:【綜合】ajax跨域問(wèn)題
什么是跨域問(wèn)題
簡(jiǎn)單來(lái)講虏束,當(dāng)前臺(tái)調(diào)用后臺(tái),如果接口不是一個(gè)域時(shí),就會(huì)產(chǎn)生跨域問(wèn)題。
由于目前前后端分離的技術(shù)架構(gòu)液茎,前臺(tái)項(xiàng)目和后臺(tái)項(xiàng)目都是獨(dú)立開(kāi)發(fā)進(jìn)行的逞姿。當(dāng)聯(lián)調(diào)測(cè)試時(shí)辞嗡,前臺(tái)頁(yè)面必然會(huì)大量調(diào)用后臺(tái)的接口,此時(shí)如果接口不是同一個(gè)域滞造,就會(huì)發(fā)生跨域問(wèn)題续室。
測(cè)試環(huán)境搭建
【后臺(tái)環(huán)境搭建】
使用node+express搭建一個(gè)后臺(tái)系統(tǒng)
(1)新建一個(gè)文件夾NodeBack,輸入 npm init 初始化項(xiàng)目谒养,創(chuàng)建index.js作為整個(gè)后臺(tái)的入口文件
(2)安裝express:npm install express --save
(3)安裝body-parse:npm install body-parse--save挺狰,用于解析post請(qǐng)求的參數(shù)
(4)編寫入口文件 index.js
var express = require('express');
/**
* 解析post來(lái)的參數(shù)
app.use(bodyParser.json()); // for parsing application/json
app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded
* */
var bodyParser = require('body-parser');
var app = express();
app.use(bodyParser.urlencoded({extended: true})); // for parsing application/x-www-form-urlencoded
app.get('/test', function (req, res) {
var name = req.query.name
var age = req.query.age
res.jsonp({
data:'name is ' + name + "| age is " + age
});
});
app.post('/login', function (req, res) {
var name = req.body.name
var password = req.body.password
if (password === "123") {
res.jsonp({
"name": name,
"password": password
});
} else {
res.send("error!");
}
});
var server = app.listen(3005, function () {
var host = server.address().address;
var port = server.address().port;
console.log('Example app listening at http://%s:%s', host, port);
});
【前臺(tái)請(qǐng)求頁(yè)面】
簡(jiǎn)單html版
簡(jiǎn)單起見(jiàn),就使用Jquery來(lái)發(fā)送前臺(tái)請(qǐng)求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!--導(dǎo)入jquery-->
<script src="http://code.jquery.com/jquery-latest.js"></script>
</head>
<body>
<a href="#" onclick="getReq()">發(fā)送get請(qǐng)求</a>
<script>
function getReq(){
$.getJSON("http://localhost:3005/test?name=hugo&&age=18")
.then((result)=>{
console.log(result)
})
}
</script>
</body>
</html>
使用firefox买窟,打開(kāi)此網(wǎng)址丰泊,按F12,調(diào)出控制臺(tái)始绍,點(diǎn)擊發(fā)出get請(qǐng)求
但是在控制臺(tái)發(fā)現(xiàn)報(bào)錯(cuò)提示瞳购,這就是跨域問(wèn)題
ajax跨域問(wèn)題分析
發(fā)生跨域問(wèn)題有三個(gè)原因:
【1】瀏覽器限制:很多同學(xué)認(rèn)為跨域問(wèn)題的是服務(wù)器后臺(tái)不允許前臺(tái)跨域訪問(wèn),但一些情況下并不是這樣亏推,還有可能是瀏覽器多管閑事学赛,處于安全考慮限制了跨域訪問(wèn)。
如何驗(yàn)證這種想法呢吞杭,我們可以在get請(qǐng)求中增加一句log
app.get('/test', function (req, res) {
var name = req.query.name
var age = req.query.age
res.jsonp({
data:'name is ' + name + "| age is " + age
});
console.log("receive get request")
});
再次發(fā)送請(qǐng)求:
發(fā)現(xiàn)響應(yīng)都是沒(méi)有任何問(wèn)題的
【2】跨域盏浇,地址中,協(xié)議芽狗、域名绢掰、端口任意一個(gè)不一樣,瀏覽器就認(rèn)為是跨域
【3】XHR(XMLHttpRequest)請(qǐng)求童擎。請(qǐng)求的type為XHR滴劲,非json、img等其他類型柔昼。
當(dāng)上述三種情況同時(shí)滿足時(shí)哑芹,跨域問(wèn)題就產(chǎn)生了,
解決思路
【1】解決瀏覽器限制:通過(guò)指定瀏覽器的啟動(dòng)參數(shù)捕透,解除限制聪姿,但是這種情況需要用戶手動(dòng)操作碴萧,所以交互不友好,所以不推薦使用
【2】轉(zhuǎn)變請(qǐng)求的類型末购。上面說(shuō)到破喻,只有請(qǐng)求類型是xhr時(shí),將xhr的類型轉(zhuǎn)化為jsonp的格式
【3】跨域盟榴。
被調(diào)用(服務(wù)器)方支持跨域曹质;
調(diào)用方做跨域支持(隱藏跨域):通過(guò)代理,從瀏覽器發(fā)送出的請(qǐng)求都是同源請(qǐng)求
全面解決跨域問(wèn)題
【禁止瀏覽器檢查跨域】
不做介紹擎场,因?yàn)閷?shí)用價(jià)值不大羽德。
【jsonp解決跨域】
json for pending :利用js請(qǐng)求資源標(biāo)簽時(shí),請(qǐng)求可以跨域迅办,來(lái)解決跨域請(qǐng)求宅静,它是一種變通的解決方案。
function jsonpReq(){
var result
$.ajax({
url:"http://localhost:3005/test?name=hugo&&age=18",
dataType:'jsonp',
success:function(json){
result=json
console.log(result)
}
})
}
在使用jsonp的時(shí)候站欺,后臺(tái)需要做改動(dòng)(java后臺(tái))姨夹,不然服務(wù)器返回的內(nèi)容,瀏覽器會(huì)當(dāng)做js代碼來(lái)解析矾策,會(huì)報(bào)語(yǔ)法錯(cuò)誤磷账。經(jīng)測(cè)試,node編寫的后臺(tái)無(wú)需添加額外的內(nèi)容贾虽。
我們對(duì)jsonp的原理進(jìn)行分析逃糟,解釋為什么java后臺(tái)需要進(jìn)行額外設(shè)置:
首先,看一下jsonp發(fā)送的報(bào)文類型是script榄鉴,在之前分析跨域問(wèn)題產(chǎn)生的條件之一就是請(qǐng)求報(bào)文的類型是xhr履磨,那么jsonp的思路就是改變報(bào)文的請(qǐng)求類型,變?yōu)閟cript庆尘。
第二剃诅,普通的ajax請(qǐng)求返回的json對(duì)象,而jsonp返回的是js腳本
第三驶忌,jsonp發(fā)送的url后面接了一串額外的callback字符
那么從這里我們就能簡(jiǎn)單分析jsonp的原理是:jsonp發(fā)送的url請(qǐng)求自動(dòng)添加了一個(gè)callback參數(shù)矛辕,當(dāng)后臺(tái)發(fā)現(xiàn)callback參數(shù)時(shí),就認(rèn)為是一個(gè)jsonp請(qǐng)求付魔,響應(yīng)頭在返回?cái)?shù)據(jù)時(shí)聊品,數(shù)據(jù)格式就由json變成script,而script的內(nèi)容就是一個(gè)函數(shù)調(diào)用几苍。那么java后臺(tái)的改動(dòng)就能分析得出如下:
所以翻屈,如果我們把url請(qǐng)求中的callback改成callback2,那么后臺(tái)就不會(huì)認(rèn)為前臺(tái)請(qǐng)求是一個(gè)jsonp請(qǐng)求妻坝。即:前臺(tái)調(diào)用的“callback”和后臺(tái)的“callback”實(shí)際上是一個(gè)接口上的協(xié)議伸眶,需要保持一致惊窖。
很明顯,Jsonp是基于Jquery的一種解決方案厘贼,但是在react界酒、vue這類本身的是要替代Jquery操作dom結(jié)構(gòu)的框架,效果可想而知嘴秸。
接下來(lái)毁欣,再來(lái)談?wù)刯sonp的利弊:
1、服務(wù)器端需要改動(dòng)岳掐。當(dāng)服務(wù)器是第三方提供時(shí)凭疮,這種做法就會(huì)出現(xiàn)局限性。
2岩四、只支持GET請(qǐng)求哭尝,通過(guò)分析原理,我們知道jsonp是通過(guò)Jquery動(dòng)態(tài)創(chuàng)建一個(gè)script來(lái)創(chuàng)建一個(gè)請(qǐng)求的剖煌,所以請(qǐng)求類型就被局限在了GET。
3逝淹、 發(fā)送的不是XHR請(qǐng)求耕姊,即XHR的諸多特性都無(wú)法使用,例如異步栅葡、事件機(jī)制都無(wú)法使用茉兰。
綜上可見(jiàn),我們?cè)诮鉀Q跨域問(wèn)題時(shí)欣簇,最好方法還是解決請(qǐng)求是跨域的规脸,而不是改變請(qǐng)求是XHR還是JSONP。
下面就繼續(xù)介紹本文的重點(diǎn):【讓服務(wù)器支持跨域】熊咽、【前臺(tái)請(qǐng)求隱藏跨域】
【從系統(tǒng)架構(gòu)分析跨域】
最常見(jiàn)的javaee 架構(gòu)
請(qǐng)求流程:
請(qǐng)求從瀏覽器發(fā)出莫鸭,首先到apache或nginx(靜態(tài)服務(wù)器) ,然后判斷客戶端請(qǐng)求是靜態(tài)請(qǐng)求還是動(dòng)態(tài)請(qǐng)求横殴。簡(jiǎn)單講被因,跟客戶端數(shù)據(jù)有關(guān)系的就是動(dòng)態(tài)數(shù)據(jù)(用戶信息,消息列表等)衫仑,而圖片梨与、js、css文狱、html等就是靜態(tài)請(qǐng)求粥鞋。
如果是靜態(tài)請(qǐng)求,靜態(tài)服務(wù)器直接處理瞄崇,然后把數(shù)據(jù)返回給客戶端呻粹。如果是動(dòng)態(tài)請(qǐng)求到踏,則轉(zhuǎn)發(fā)給后臺(tái)的應(yīng)用>服務(wù)器,處理完畢后尚猿,應(yīng)用服務(wù)器把數(shù)據(jù)返回給靜態(tài)服務(wù)窝稿,隨后靜態(tài)服務(wù)器發(fā)送給客戶端。
中間的http服務(wù)器凿掂,一般有兩個(gè)作用:
(1)處理靜態(tài)請(qǐng)求
(2)轉(zhuǎn)發(fā)動(dòng)態(tài)請(qǐng)求和負(fù)載均衡伴榔。
那么在看跨域請(qǐng)求時(shí)的兩種思路:
第一:直接從瀏覽器里發(fā)出請(qǐng)求到被調(diào)用方的http服務(wù)器。 這時(shí)需要在http服務(wù)器上做響應(yīng)的修改庄萎,這些修改都是基于http請(qǐng)求關(guān)于跨域部分的協(xié)議踪少。就是在返回頭里增加一些字段,告訴瀏覽器被調(diào)用方允許客戶端跨域訪問(wèn)糠涛。這樣瀏覽器就不會(huì)報(bào)跨域的問(wèn)題援奢。
第二:隱藏跨域。請(qǐng)求不直接從瀏覽器發(fā)出忍捡,而是從中間http服務(wù)器轉(zhuǎn)出集漾。通過(guò)服務(wù)器的轉(zhuǎn)發(fā),瀏覽器不會(huì)發(fā)現(xiàn)所有的請(qǐng)求都是同一個(gè)域砸脊。即調(diào)用方會(huì)發(fā)現(xiàn)所有的請(qǐng)求都是從本地的http服務(wù)器發(fā)出(其實(shí)并不是具篇,請(qǐng)求都是經(jīng)過(guò)客戶端http服務(wù)器的轉(zhuǎn)發(fā))
跨域解決方向-被調(diào)用方解決-Filter及spring解決方案
這是基于支持跨域的解決思路,是基于http協(xié)議關(guān)于跨域方面的一些規(guī)定凌埂,在響應(yīng)頭里增加指定的字段驱显,告訴瀏覽器服務(wù)端支持跨域調(diào)用。 此種情況下瞳抓,瀏覽器直接發(fā)送請(qǐng)求到服務(wù)方埃疫。
在據(jù)圖解釋此方案前,我們理清幾個(gè)問(wèn)題:
瀏覽器如何判斷是否跨域孩哑?
答:通過(guò)對(duì)比普通請(qǐng)求和跨域請(qǐng)求瀏覽器里的請(qǐng)求頭里的內(nèi)容栓霜,可以發(fā)現(xiàn)跨域請(qǐng)求中增加了一個(gè)origin字段,字段值為當(dāng)前域名的信息臭笆。也就是說(shuō)叙淌,當(dāng)瀏覽器發(fā)現(xiàn)請(qǐng)求是跨域時(shí),就會(huì)在請(qǐng)求頭添加origin的字段愁铺。當(dāng)瀏覽器接收到響應(yīng)頭后鹰霍,就會(huì)檢查里面是否有允許跨域的字段,如果沒(méi)有茵乱,就會(huì)報(bào)錯(cuò)茂洒。
瀏覽器是先執(zhí)行?還是先判斷瓶竭?
答:瀏覽器發(fā)送跨域請(qǐng)求給后臺(tái)督勺,后臺(tái)日志打印正常渠羞,瀏覽器響應(yīng)狀態(tài)碼為200,但卻報(bào)跨域問(wèn)題智哀,說(shuō)明瀏覽器是先執(zhí)行請(qǐng)求操作次询,然后在判斷是否跨域。那么瓷叫,是不是所有的請(qǐng)求都是先執(zhí)行后判斷呢屯吊?
并不是的,這就引出了簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求的概念摹菠。
簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求
每次瀏覽器再發(fā)送請(qǐng)求時(shí)盒卸,會(huì)做一次判斷,如果是簡(jiǎn)單請(qǐng)求次氨,那么就是先執(zhí)行再判斷蔽介。如果是非簡(jiǎn)單請(qǐng)求,那么瀏覽器會(huì)先發(fā)送一個(gè)預(yù)檢頭煮寡,檢查通過(guò)后虹蓄,瀏覽器才會(huì)發(fā)送真正的請(qǐng)求
跨域解決——服務(wù)器端實(shí)現(xiàn)【Spring 編寫的后臺(tái)】
基于注解,可以使用
@CrossOrigin
那么被注解類下的所有請(qǐng)求方法都是可以支持跨域的
跨域解決——服務(wù)器端實(shí)現(xiàn)【Spring boot 編寫的后臺(tái)】
這里我們需要了解http協(xié)議關(guān)于跨域方面所有的要求洲押,針對(duì)不同的場(chǎng)景返回不同的頭武花。只有知道了所有的響應(yīng)頭,我們才能在后面的http服務(wù)器上配置響應(yīng)頭杈帐。
首先需要注冊(cè)一個(gè)filterRegistrationBean ,然后讓所有的請(qǐng)求都經(jīng)過(guò)這filter
接下來(lái)我們?cè)?strong>CrosFilter里的doFilter方法中添加服務(wù)器響應(yīng)頭
對(duì)于非簡(jiǎn)單請(qǐng)求专钉,我們?cè)诎凑丈鲜龇椒ㄌ砑禹憫?yīng)頭后挑童,可能會(huì)出現(xiàn)如下的錯(cuò)誤:
意思是我們的返回頭缺少“access-control-allow-headers”是缺少的。
所以我們需要在后臺(tái)服務(wù)器中再增加一行代碼:
res.addHeader("Access-Control-Allow-Headers","Content-type");
在上面我們分析了什么是簡(jiǎn)單請(qǐng)求和非簡(jiǎn)單請(qǐng)求跃须,那么在非簡(jiǎn)單請(qǐng)求的情況下站叼,瀏覽器會(huì)發(fā)送兩次請(qǐng)求:
這樣會(huì)影響請(qǐng)求的效率。http協(xié)議中增添了一個(gè)請(qǐng)求頭菇民,對(duì)預(yù)檢頭緩存尽楔。
res.addHeader("Access-Control-Max-Age","3600");
告訴瀏覽器緩存預(yù)檢頭,時(shí)間為3600s第练。那么當(dāng)再次發(fā)送跨域請(qǐng)求時(shí)阔馋,就只會(huì)請(qǐng)求一次。
問(wèn)題:*代表所有的Url請(qǐng)求娇掏,那么是不是在任何情況下都沒(méi)有問(wèn)題呢呕寝?
帶自定義頭的跨域
當(dāng)我們直接在跨域請(qǐng)求中添加自定義請(qǐng)求頭時(shí)(假設(shè)自定義請(qǐng)求頭為x-header1和x-header2),可能會(huì)報(bào)如下錯(cuò)誤:
這里的解決辦法是將自定義的請(qǐng)求頭添加到Access-Control_Allow-Headers:
res.addHeader("Access-Control_Allow-Headers","Content-type,x-header1,x-header2");
當(dāng)然我們也可以動(dòng)態(tài)地設(shè)置headers
跨域解決——服務(wù)器端實(shí)現(xiàn)【Node.js 編寫的后臺(tái)】
跨域解決方向-被調(diào)用方解決-Http服務(wù)器解決方案
【Nginx配置】
Nginx配置文件:
【apache配置】
相當(dāng)于把之前在nginx上的配置婴梧,再在apache上配置一下下梢,思路也是和nginx一樣的客蹋。由于apache配置比較復(fù)雜,詳細(xì)配置請(qǐng)參考其他文章孽江。
跨域解決方向-調(diào)用方解決
這是基于隱藏跨域的解決思路讶坯。此種情況下,請(qǐng)求不會(huì)直接從瀏覽器發(fā)送到被調(diào)用方岗屏,而是從調(diào)用方的http服務(wù)器轉(zhuǎn)發(fā)過(guò)去的(被調(diào)用方http服務(wù)器或者應(yīng)用服務(wù)器接收)辆琅。使用了反向代理技術(shù),在瀏覽器上是看不到任何跨域請(qǐng)求担汤。
這里就和上述apache和nginx操作類型涎跨,需要修改他們的配置文件(反向代理的配置)。本文只做簡(jiǎn)單的配置解釋崭歧,詳細(xì)內(nèi)容請(qǐng)小伙伴們自行百度隅很。
Nginx反向代理
首先我們修改一下host,其效果是當(dāng)你在瀏覽器訪問(wèn) www.a.com和www.b.com時(shí)率碾,url地址會(huì)映射到127.0.0.1上
接下來(lái)配置Nginx配置
然后客戶端在進(jìn)行網(wǎng)絡(luò)請(qǐng)求時(shí)叔营,需要變成nginx代理之后的地址,也就是
/ajaxserver
可以發(fā)現(xiàn)所宰,此時(shí)調(diào)用的url就是本域的地址(瀏覽器看到的是相對(duì)地址)绒尊,自然就不存在了跨域問(wèn)題。
apache反向代理配置
思路:增加一個(gè)虛擬主機(jī)仔粥,然后讓此虛擬主機(jī)婴谱,把我們的跨域請(qǐng)求作反向代理
總結(jié)
至此,本文簡(jiǎn)單分析了一下跨域問(wèn)題產(chǎn)生的原因和常見(jiàn)的解決方法躯泰,如果有什么問(wèn)題歡迎留言交流討論~
筆者個(gè)人訂閱號(hào)~歡迎小伙伴們關(guān)注
若有疑問(wèn)可以QQ聯(lián)系筆者谭羔,雖然不一定100%解決你的問(wèn)題,但是可以交流探討一波:2276604211