1、問題背景:
公司在做自己的OA系統,採用的前後端分離的開放模式,在與前端對接"獲取當前用戶信息"這個接口時,瀏覽器的控制檯就報了以下錯誤:
Access to XMLHttpRequest at 'http://192.168.0.101:10000/api/login/login?username=1&password=1' from origin 'http://localhost:63342' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
我們採用token來維護用戶登錄狀態的方案,用戶登錄成功後,以後每次需要登錄的接口都需要在header中帶着這個token,後端用springMVC的攔截器做登錄攔截,一切看着都是這麼的自然,然而......
第一個對接的是"登錄接口",很順利的通過了。當前端調用第二個接口——"獲取當前用戶信息"接口時,突然前端小姐姐叫了一聲:"哎?這是什麼錯誤?是不是跨域了?"。我當時心想:"不可能呀,我在搭框架的時候已經在後端處理了跨域問題"。下面是我的解決方式:
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //表示所有的資源都允許跨域訪問
.allowedOrigins("*") //表示所有的源都可以訪問
////表示允許那些http請求方式
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true) //表示請求中可以攜帶cookie
.maxAge(3600) //表示預檢請求的有效期,有效期內不會在發送第二次預檢請求。
.allowedHeaders("*"); //表示允許所有請求頭
}
標準的CORS解決方式,也確實能解決跨域問題,但是並不適合我這裏的場景,爲什麼?因爲我使用了攔截器。爲什麼使用了攔截器就不行了?下面聽我分析一下:
2、問題原因:
爲什麼第一個登錄接口沒事,第二個就有跨域問題?因爲第一個登陸接口我沒做登錄攔截,會執行到上面的方法(此時該登錄請求是一個簡單請求)。第二個接口做了登錄攔截,##但我在請求頭中加入了token值,還是應該可以通過攔截器到達目標方法,也會執行上面的跨域解決方法,爲什麼還是被攔截出現了跨域問題呢?##OK,問題就出現在這裏,就是因爲我在請求頭中加入了token,導致了該請求從一個簡單請求變成了非簡單請求,至於什麼是非簡單請求暫時先不說?但他有一個區別於簡單請求的最大區別:會在正式請求發送之前,先發送一個預檢請求,就是個普通請求,用於檢查目標服務器是否允許當前源跨域訪問。該預檢請求可是什麼參數都不帶的,自然就被我的登錄攔截器給攔下了,然後回去告訴客戶端瀏覽器說目標服務器不允許你跨域訪問,瀏覽器說了聲,哦~,然後就向前端小姐姐拋了一個錯誤,可是把小姐姐難住了~~
3、解決方案:
既然該預檢請求會被攔截器攔截,導致無法執行我解決跨域的方法,那我只需要在攔截器攔截之前,給請求的響應頭加入允許跨域訪問的響應頭即可。springMVC的本質是servlet,當時學javaWeb時知道filter的執行時機是在servlet之前,那我使用filter來增加響應頭就行了唄。
自己寫一個filter也行,但其實springMVC早就提供了這麼一個解決跨域的filter——CorsFilter。若是springBoot項目的話,直接在配置類中加入該bean即可。
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("*");//允許所有的源訪問
config.setAllowCredentials(true);//允許請求中帶着cookie
config.addAllowedMethod("*");//允許哪些http請求方式
config.addAllowedHeader("*");//允許請求頭中額外帶着哪些請求頭
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);//表示所有的資源都允許跨域訪問
return new CorsFilter(configSource);
}
其他的解決方式也有很多,這裏我再提供幾種後端解決跨域的方式:
1)自定義filter
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE,PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with");
filterChain.doFilter(servletRequest, servletResponse);
}
2)重寫springMVC的addCorsMappings方法
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") //表示所有的資源都允許跨域訪問
.allowedOrigins("*") //表示所有的源都可以訪問
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") //表示允許那些http請求方式
.allowCredentials(true) //表示請求中可以攜帶cookie
.maxAge(3600) //表示預檢請求的有效期,有效期內不會在發送第二次預檢請求。
.allowedHeaders("*"); //表示允許所有請求頭
}
3)使用@CrossOrigin註解:更細粒度的跨域訪問控制方式
注:可用在controller上或方法上,會覆蓋全局的跨域配置
@Controller
@RequestMapping("/home")
@CrossOrigin(origins = "*", maxAge = 3600)
public class HomeController {
@RequestMapping("/index")
public String index(){
return "index";
}
}
以上四種方式,根據實際項目中攔截器的使用情況靈活選用,但還是最推薦filter的方式,因爲最強大和通用。
4、相關知識
1)簡單請求和非簡單請求
瀏覽器將跨域請求分爲簡單請求和非簡單請求。只要同時滿足以下兩大條件,就屬於簡單請求:
(1) 請求方法是以下三種方法之一:
HEAD
GET
POST
(2)HTTP的頭信息不超出以下幾種字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain
以上任何一個條件不滿足的,就屬於非簡單請求。非簡單請求就會額外發送"預檢請求"。
2)跨域及產生的原因
跨域的定義:即通訊協議、ip、端口三個有任何一個不相同就被稱爲跨域。如http://localhost:9000/a.html,要去訪問http://localhost:10000/api/getUser接口,就稱之爲跨域請求,因爲兩者的端口不一樣。此時如果不做處理的話,兩者是無法進行通信的。
跨域產生的原因:就是瀏覽器的同源策略。該策略是在1995年由網景公司引入的,目前各瀏覽器都支持該策略。該策略的引入也是出於對瀏覽器訪問的一種安全機制,具體例子這裏就不細說了,大家去問度娘吧。
3)標準的解決的方案Cors
Cors是W3C組織引入的,全稱爲(Cross-origin resource sharing)跨域資源共享,是用來解決瀏覽器跨域訪問問題的一種標準的行業規範,目前各大瀏覽器都支持Cors。
Cors的核心思想是:通過服務器端增加一系列的允許跨域的響應頭,來解決跨域問題。
5、結束語
好了,今天就到這裏吧,能把以上部分搞明白,日常開發中的跨域問題基本上都能解決了。
最後說一下我的心聲:
在我遇到本文開頭的那個問題之前,我以爲我已經能解決跨域問題了,直到解決那個問題之後,我才發現我所知道的也只是浮於表面的一些東西,更深入的東西還會有,只是我現在還沒碰到而已。所以不要自滿,不要自以爲我最牛逼,別人都是傻逼,每個人都要他所擅長的地方,只是你還沒發現而已。
你知道的越多,你就知道的越少,這句話至少在計算機領域是很經典的。