前言
目前在開發(fā)中前后端分離的模式比較普遍售滤,那么跨域問題也就時常會遇到。網(wǎng)上資料都很片面台诗,不全面完箩,以及都沒有說為什么這么解決。
本文會通過前端ajax訪問java后端接口的場景拉队,分別從瀏覽器弊知、后端響應(yīng)頭設(shè)置、代理服務(wù)器apache和nginx配置粱快、調(diào)用端反向代理等方面考慮跨域解決方案秩彤。從簡單請求叔扼、非簡單請求和帶cookie的請求等多種請求方式逐步分析如何規(guī)避跨域限制。
內(nèi)容詳情
什么是跨域漫雷?狹義的理解跨域是指受到瀏覽器同源策略限制的一類請求瓜富,通常我們說的跨域就是指的這一類請求。當(dāng)協(xié)議降盹、域名(包含子域名)与柑、端口號中任意一個不相同時,都屬于不同域蓄坏。不同域之間相互請求資源价捧,就會受到瀏覽器的同源策略限制。
同源策略
同源策略是一種約定涡戳,由Netscape公司1995年引入瀏覽器结蟋,是瀏覽器最核心也最基本的安全功能。保證用戶信息的安全渔彰,防止惡意的網(wǎng)站竊取數(shù)據(jù)椎眯。比較常見的就是XSS、CSFR等攻擊胳岂。
既然有安全問題,那為什么又要跨域呢舔稀? 舉個例子乳丰,假如公司內(nèi)部有多個不同的子域,一個是location.company.com ,另一個是app.company.com , 這時想從 app.company.com去訪問 location.company.com 的資源就需要跨域内贮。
ajax跨域請求
下面产园,通過ajax訪問不同域的后端java接口的案例來分析,如何規(guī)避這種限制夜郁。(后面我們把這個案例稱為案例一)
- 首先新建spring boot項(xiàng)目A什燕,端口號使用默認(rèn)的8080,快速開發(fā)一個java接口如下
@RestController
@RequestMapping("/getData ")
public class GetDataController {
@GetMapping("/getFirstData")
private ResultBean getFirstData() {
System.out.println("getFirstData success");
return new ResultBean("getFirstData success");
}
}
- 再次新建一個spring boot項(xiàng)目B竞端,端口號設(shè)置為8081屎即,編輯前端頁面如下:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<script src="jquery-1.9.1.min.js"></script>
<script type="text/javascript">
function getFirstData(){
$.ajax({
type : "GET",
url:"http://localhost:8080/getData/getFirstData",
success:function(json){
console.log(json);
}
});
}
</script>
</head>
<body>
<a href="#" onclick="getFirstData()">發(fā)送getFirstData請求</a>
</body>
</html>
- 然后啟動兩個項(xiàng)目,瀏覽器中訪問 http://localhost:8080/getData/getFirstData事富,正常返回json數(shù)據(jù){"data":"getFirstData success"}技俐,此時說明java接口正常。
- 接著瀏覽器先訪問項(xiàng)目B的頁面http://localhost:8081/index.html统台,點(diǎn)擊頁面上的a標(biāo)簽雕擂,結(jié)果瀏覽器控制臺并沒有打印出接口預(yù)期返回的json數(shù)據(jù)。
-
查看A項(xiàng)目的控制臺贱勃,輸出了"getFirstData success"井赌。在瀏覽器開發(fā)者模式下查看網(wǎng)絡(luò)谤逼,發(fā)現(xiàn)請求的狀態(tài)為200 ,但是出現(xiàn)了以下錯誤提示仇穗。
結(jié)論:跨域并不是請求發(fā)不出去流部,請求能發(fā)出去,服務(wù)端能收到請求并正常返回結(jié)果仪缸,只是結(jié)果被瀏覽器攔截了贵涵。
瀏覽器端解決跨域
根據(jù)上面的結(jié)論,我們可以讓瀏覽器不做限制恰画,以chorme瀏覽器為例宾茂, 如果設(shè)置為支持跨域模式,只需以下幾步拴还。
- 在電腦上新建一個目錄跨晴,例如:C:\MyChromeDevUserData。這是保存?zhèn)€人信息的目錄片林,是chrome瀏覽器防止用戶使用跨域模式泄露自己的個人信息采用的措施端盆,這里不詳細(xì)探討。
- 在屬性頁面中的目標(biāo)輸入框里加上 --disable-web-security --user-data-dir=C:\MyChromeDevUserData费封,--user-data-dir的值就是剛才新建的目錄焕妙。
- 點(diǎn)擊應(yīng)用和確定后關(guān)閉屬性頁面,并打開chrome瀏覽器弓摘。發(fā)現(xiàn)有“--disable-web-security”相關(guān)的提示焚鹊,說明chrome能正常跨域工作了韧献。
說明:這種解決方式意義不大末患,因?yàn)樾枰械目蛻舳俗龈膭樱⑶颐總€用戶使用的瀏覽器也各不相同锤窑,處理的方式也不同璧针。在這里介紹這種解決方式,只是為了更進(jìn)一步說明渊啰,產(chǎn)生跨域訪問限制的根源就是瀏覽器探橱。
script標(biāo)簽解決跨域
其實(shí)通過script標(biāo)簽可以跨域請求數(shù)據(jù)的,如下虽抄,在B項(xiàng)目的頁面中訪問百度的一個接口
效果如下:
以上是在案例一中B項(xiàng)目的域里走搁,通過動態(tài)創(chuàng)建script標(biāo)簽,跨域訪問百度的接口迈窟,并成功獲取到了資源私植。這種請求屬于script請求,可見瀏覽器并不會限制這類請求车酣。詳細(xì)原因可以查看同源策略文檔曲稼。
但是這種方式需要服務(wù)端提供一種約定索绪,約定請求的參數(shù)里面如果包含指定的參數(shù)(比如上面的cb參數(shù)),會把原來的返回對象先轉(zhuǎn)變成js代碼然后返回給瀏覽器解析贫悄,而其中Js代碼是函數(shù)調(diào)用的形式瑞驱,比如上例中百度接口返回的函數(shù)的函數(shù)名是cb的值,函數(shù)的參數(shù)就是原來需要返回的結(jié)果對象窄坦。
假如沒有傳遞cb參數(shù)唤反,請求可以正常訪問到數(shù)據(jù)并返回,但是瀏覽器判斷是script請求鸭津,所以仍然會以解析腳本的格式解析返回的結(jié)果彤侍,后果就是無法解析(Uncaught TypeError)。
通過案例一的請求過程逆趋,我們發(fā)現(xiàn)ajax的請求類型是XHR(XMLHttpRequest)類型盏阶,如下。
注:XHR獲取數(shù)據(jù)的目的是為了持續(xù)修改一個加載過的頁面闻书,是Ajax設(shè)計(jì)的底層概念名斟,想了解XHR可以點(diǎn)擊這里。*
大膽的猜想一下魄眉,是否可以讓ajax發(fā)出的請求封裝成script砰盐,然后由script向后端發(fā)出請求,這樣瀏覽器就不會做限制了坑律。
那么這也正是接下來要介紹的jsonp的解決思路楞卡。
jsonp解決跨域
- 什么是jsonp
- 全稱是json padding,請求時通過動態(tài)創(chuàng)建一個script脾歇,在script中發(fā)出請求,通過這種變通的方式讓請求資源可以跨域淘捡。
- 它不是一個官方協(xié)議藕各,是一個約定,約定請求的參數(shù)里面如果包含指定的參數(shù)(默認(rèn)是callback)焦除,就說明是一個jsonp請求激况,服務(wù)器發(fā)現(xiàn)是jsonp請求,就會把原來的返回對象變成js代碼膘魄。Js代碼是函數(shù)調(diào)用的形式乌逐,它的函數(shù)名是callback的值,它的函數(shù)的參數(shù)就是原來需要返回的結(jié)果创葡。
- jsonp的實(shí)現(xiàn)方式
- 通過例子來說明Jsonp的請求方式,如下
$.ajax({
url:"http://localhost:8080/getData/getSecondData",
dataType:"jsonp",
jsonp:"callback",
success:function(json){
console.log(json);
}
});
-
查看瀏覽器網(wǎng)絡(luò),發(fā)現(xiàn)jsonp發(fā)出去的請求是script類型腻贰。因?yàn)閯討B(tài)創(chuàng)建的script標(biāo)簽在發(fā)送請求以后會馬上被刪除,所以在瀏覽器中無法查看的到胰舆,我們可以采用斷點(diǎn)查看。
- 當(dāng)jquery-1.9.1.js中的代碼執(zhí)行完如上的位置時蹬挤,在頁面上動態(tài)創(chuàng)建了一個script缚窿。如下
<script async="" src="http://localhost:8080/getData/getSecondData? callback=jQuery19102645927304195774_1530774759805&_=1530774759806">
</script>
可以看到j(luò)sonp在請求url后面追加了兩個參數(shù),callback和一個下劃線作為參數(shù)名的參數(shù)焰扳,這個callback就是上面提到的約定參數(shù)倦零,而下劃線作為參數(shù)名的參數(shù)值是一個隨機(jī)數(shù),作用是為了防止請求的結(jié)果被緩存了吨悍,如果想讓結(jié)果被緩存可以添加cache:true扫茅,如
$.ajax({
url:"http://localhost:8080/getData/getSecondData",
dataType:"jsonp",
jsonp:"callback",
cache:true,
success:function(json){
console.log(json);
}
});
此時后臺不做改動的話返回的還是json對象,瀏覽器把對象當(dāng)做script對象解析畜份,所以會報(bào)錯诞帐。如下可以看到返回的參數(shù)類型。
接下來修改后臺代碼爆雹,給提供接口的controller提供一個切面停蕉,返回“callback”,編碼如下
@ControllerAdvice
public class JsonpAdvice extends AbstractJsonpResponseBodyAdvice{
public JsonpAdvice(){
super("callback"); //其中參數(shù)“callback”可以修改钙态,但是必須與頁面請求的回調(diào)參數(shù)對應(yīng)
}
}
CORS解決跨域方案
- 引言
回到案例一慧起,來看看跨域所報(bào)的錯誤,大概意思是說請求的資源上沒有“Access-Control-Allow-Origin”頭信息(此處說的是響應(yīng)頭)册倒。
那么可以從這個地方考慮蚓挤,在返回資源的時候加上這個頭信息。這也就是接下來說的CORS請求的解決思路驻子。
CORS是W3C標(biāo)準(zhǔn), 全名叫跨域資源共享Cross-origin resource sharing灿意,允許瀏覽器向跨域服務(wù)器發(fā)出XMLHttpRequest請求。
- 簡單請求跨域
當(dāng)我們在做跨域請求資源的時候崇呵,會發(fā)現(xiàn)多了Origin的字段(此字段指定了當(dāng)前頁的域名和端口號缤剧,它的值是由瀏覽器自動獲取的,無法通過手動修改) 域慷,如下:
然后在響應(yīng)的時候荒辕,瀏覽器會判斷響應(yīng)頭里面有沒有跨域信息,如果沒有就會報(bào)錯犹褒。
那我們嘗試修改后臺代碼,在響應(yīng)頭里添加這個信息叠骑。
@Bean
public FilterRegistrationBean registerFilter(){
FilterRegistrationBean frBean = new FilterRegistrationBean();
frBean.addUrlPatterns("/*");
frBean.setFilter(new CrosFilter());
return frBean;
}
以下是過濾器實(shí)現(xiàn)部分李皇。
public class CrosFilter implements Filter {
@Override
public void destroy() { }
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse hsr =(HttpServletResponse)response;
hsr.addHeader("Access-Control-Allow-Origin","http://localhost:8081"); //添加Origin
filterChain.doFilter(request,response);
}
@Override
public void init(FilterConfig arg0) throws ServletException { }
}
此時就可以通過ajax訪問到資源了,這種情況只能允許一種域名訪問,如果想讓多個域名都訪問到此接口,可以用*號代替上面設(shè)置的參數(shù)变勇。如
hsr.addHeader("Access-Control-Allow-Origin", "*");
- 預(yù)檢命令
其實(shí)瀏覽器將CORS分為兩類, 簡單請求和非簡單請求呀袱,每次請求會先判斷是否為簡單請求。
- 如果是簡單請求就先執(zhí)行后判斷資源信息是否允許跨域,這也是案例一中為什么請求的狀態(tài)是200馒铃,但是無法獲取數(shù)據(jù)的原因了痕惋。
- 如果不是簡單請求會先發(fā)一個預(yù)檢命令,檢查通過以后才會把跨域請求發(fā)送過去娃殖。
- 像PUT值戳,DELETE方法的ajax請求就屬于非簡單請求,而像GET炉爆、HEAD堕虹、POST方法的ajax請求,如果不考慮其他因素都屬于簡單請求芬首,但是帶json參數(shù)或者自定義頭的ajax請求就屬于非簡單請求赴捞。
如下,實(shí)現(xiàn)一種帶json參數(shù)的ajax請求
var params={username :"user", password:"123"};
function getFirstData(){
$.ajax({
type : "POST",
data: JSON.stringify(params),
url:"http://localhost:8080/getData/postUser",
contentType:"application/json;charset=UTF-8",
success:function(json){
console.log(json);
}
});
}
后臺代碼
@PostMapping("/postUser")
private DataSource postUser(@RequestBody User user){
System.out.println("postUser success");
return new DataSource("postUser success");
}
如下郁稍,訪問了一次赦政,出現(xiàn)了兩條請求數(shù)據(jù)。
第一條OPTIONS方法的請求就是預(yù)檢請求耀怜,通過實(shí)例測試會發(fā)現(xiàn)恢着,在預(yù)檢的時候,請求頭里面會出現(xiàn)一個頭信息
Access-Control-Request-Headers:content-type
意思是說它會詢問一下后臺服務(wù)器是否允許這個頭封寞,如果響應(yīng)頭里沒有對應(yīng)的信息就會報(bào)錯然评,所以跨域請求就失敗了。如下
因此我們需要在過濾器中加上對應(yīng)的響應(yīng)頭狈究,如下: hsr.addHeader("Access-Control-Allow-Headers", "Content-Type");
到這里有個問題了碗淌,如果每次請求都會預(yù)檢未免多此一舉,那么我們可以利用下面這個響應(yīng)頭設(shè)置預(yù)檢結(jié)果緩存時間抖锥,單位為秒亿眠。 hsr.addHeader("Access-Control-Max-Age", "3600"); 這樣設(shè)置以后,瀏覽器再次訪問此域名時磅废,一個小時內(nèi)都不用預(yù)檢纳像。
攜帶cookie跨域請求
還有一種情況,在請求資源的時候往往需要帶上cookie信息拯勉,cookie中記錄了用戶的信息以及session會話的id等竟趾。可以用下面這種方式宫峦,在ajax請求中攜帶cookie信息岔帽。
$.ajax({
type : "GET",
url:"http://localhost:8080/getData/getCookie",
xhrFields{
withCredentials:true
},
success:function(json){
console.log(json);
}
});
后臺代碼
@GetMapping("/getCookie")
private DataSource getCookie(@CookieValue(value="name")String cookie){
System.out.println("getCookie success");
return new DataSource("getCookie success");
}
然后在過濾器中還需要設(shè)置響應(yīng)頭,如:hsr.addHeader(“Access-Control-Allow-Credentials”,”true”);
接著在瀏覽器控制臺下添加一個cookie信息导绷,如:document.cookie=“name=tj”
測試發(fā)現(xiàn)犀勒,如果響應(yīng)頭里是設(shè)置了hsr.addHeader("Access-Control-Allow-Origin", ""),是不允許通過的,需要設(shè)置全匹配贾费,不能用通配符钦购。
如果我們需要支持多個域名可以訪問,服務(wù)端可以先從request中將origin中的域名信息取出來褂萧,然后賦值給響應(yīng)頭的origin就可以了押桃。
HttpServletRequest req =(HttpServletRequest) request;
String origin = req.getHeader("origin");
if(origin!=null){
hsr.addHeader("Access-Control-Allow-Origin",origin);
}
自定義頭的跨域請求
還有一種自定義頭的跨域也屬于非簡單跨域,解決方式和cookie的類似箱玷。
添加頭的操作
type:"get",
url:"http://localhost:8080/getData/getFourthData",
headers:{
"myheader":"qunar"
},
success:function(json){
console.log(json);
}
});
后臺先從request中取出頭信息怨规,然后判斷是否為空,然后賦值給響應(yīng)頭锡足。
String headers =req.getHeader("Access-Control-Request-Headers");
if(headers!=null){
hsr.addHeader("Access-Control-Allow-Headers",headers);
}
如此便成功完成了跨域請求波丰。下面是攔截器中完整的配置。
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse hsr = (HttpServletResponse)response;
HttpServletRequest req = (HttpServletRequest) request;
String origin = req.getHeader("origin");
String headers = req.getHeader("Access-Control-Request-Headers");
if(origin!=null){
hsr.addHeader("Access-Control-Allow-Origin", origin);
}
if(headers!=null){
hsr.addHeader("Access-Control-Allow-Headers", headers);
}
hsr.addHeader("Access-Control-Max-Age", "3600");
hsr.addHeader("Access-Control-Allow-Credentials","true");
hsr.addHeader("Acccess-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT");
filterChain.doFilter(request, response);
}
springMVC注解實(shí)現(xiàn)跨域請求
其實(shí)后端實(shí)現(xiàn)跨域請求并不用這么麻煩舶得,springMVC的4.2版本以后提供了注解的方式解決跨域問題掰烟,在類上添加CrossOrigin注解,如下
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/getData")
public class GetDataController {
...//此處省略
}
注釋掉前面過濾器的配置沐批,上面的所有請求也都能訪問了纫骑。
小結(jié): 有如此利器,還要介紹前面的解決方案九孩,是為了更好的理解跨域解決思路先馆。在不滿足注解的框架中也能很好的實(shí)現(xiàn)跨域請求。
代理服務(wù)器實(shí)現(xiàn)跨域
到這里躺彬,基本上已經(jīng)了解了java后端解決跨域的辦法煤墙。而實(shí)際應(yīng)用的部署環(huán)境往往會添加代理服務(wù)器,如下
那么我們可以考慮從代理服務(wù)器端解決跨域問題宪拥。解決的思路相同仿野,直接說配置方式。
被調(diào)用端支持跨域配置
這種場景配置的是被調(diào)用端的代理服務(wù)器她君。在瀏覽器某個域(http://location.company.com)中請求不同域(http://app.company.com)的資源脚作,首先將請求發(fā)送給被調(diào)用端的代理服務(wù)器,由代理服務(wù)器將請求路由到相應(yīng)的資源服務(wù)器缔刹,而資源服務(wù)器并不用管請求方是誰球涛,這種代理方式我們稱之為正向代理。
Nginx中配置
在nginx.conf文件中配置如下校镐。
注意:請求頭的參數(shù)在這里都需要小寫亿扁,并且“-”需要轉(zhuǎn)成下劃線, If后面需要帶上空格灭翔,否則語法會報(bào)錯。
Apache中配置
在httpd-vhosts.conf中配置虛擬主機(jī)相關(guān)配置。
注意:在httpd.conf中將vhost相關(guān)配置打開肝箱。并且將proxy模塊哄褒、proxy http模塊、Heard模塊煌张、rewrite模塊打開呐赡。
調(diào)用端反向代理實(shí)現(xiàn)跨域
以上都是從被調(diào)用端來解決的,屬于支持跨域骏融。當(dāng)無法修改被調(diào)用方的時候链嘀,可以配置調(diào)用端代理服務(wù)器來實(shí)現(xiàn)跨域。瀏覽器向同一域下的反向代理服務(wù)器發(fā)出請求档玻,再由反向代理服務(wù)器轉(zhuǎn)發(fā)怀泊,向其他域請求資源并返回給瀏覽器,瀏覽器不知道請求的資源在哪個服務(wù)器上误趴,這種代理方式我們稱之為反向代理霹琼。
Nginx中配置
Apache配置
總結(jié)
全文主要是通過ajax請求不同域下接口資源的場景,詳細(xì)的分析了跨域的原理以及如何規(guī)避這種跨域凉当。首先介紹了瀏覽器端解決跨域枣申,script標(biāo)簽解決跨域和jsonp的方式解決跨域,但是這些方式都有明顯的缺陷看杭,接著重點(diǎn)介紹了cors如何解決跨域忠藤,相信通過本文,可以對跨域有一定的理解和解決思路了楼雹。