跨域問(wèn)題的由來(lái)
相信很多人都或多或少了解過(guò)跨域問(wèn)題,尤其在現(xiàn)如今前后端分離大行其道的時(shí)候。
你在本地開(kāi)發(fā)一個(gè)前端項(xiàng)目,這個(gè)項(xiàng)目是通過(guò) node 運(yùn)行的,端口是9528,而服務(wù)端是通過(guò) spring boot 提供的,端口號(hào)是7001。
當(dāng)你調(diào)用一個(gè)服務(wù)端接口時(shí),很可能得到類似下面這樣的一個(gè)錯(cuò)誤:

然后你在發(fā)送請(qǐng)求的地方debug,在出現(xiàn)異常的地方你將得到這樣的結(jié)果:

異常對(duì)象很詭異,返回的 response 是 undefined 的,并且 message 消息中只有一個(gè)"Network Error"。
看到這里你應(yīng)該要知道,你遇到跨域問(wèn)題了。
但是你需要明確的一點(diǎn)是,這個(gè)請(qǐng)求已經(jīng)發(fā)出去了,服務(wù)端也接收到并處理了,但是返回的響應(yīng)結(jié)果不是瀏覽器想要的結(jié)果,所以瀏覽器將這個(gè)響應(yīng)的結(jié)果給攔截了,這就是為什么你看到的response是undefined。
瀏覽器的同源策略
那瀏覽器為什么會(huì)將服務(wù)端返回的結(jié)果攔截掉呢?
這就需要我們了解瀏覽器基于安全方面的考慮,而引入的 同源策略(same-origin policy) 了。
早在1995年,Netscape 公司就在瀏覽器中引入了“同源策略”。
最初的 “同源策略”,主要是限制Cookie的訪問(wèn),A網(wǎng)頁(yè)設(shè)置的 Cookie,B網(wǎng)頁(yè)無(wú)法訪問(wèn),除非B網(wǎng)頁(yè)和A網(wǎng)頁(yè)是“同源”的。
那么怎么確定兩個(gè)網(wǎng)頁(yè)是不是“同源”呢,所謂“同源”就是指"協(xié)議+域名+端口"三者相同,即便兩個(gè)不同的域名指向同一個(gè)ip地址,也非同源。

沒(méi)有同源策略的保護(hù)
那么為什么要做這個(gè)同源的限制呢?因?yàn)槿绻麤](méi)有同源策略的保護(hù),瀏覽器將沒(méi)有任何安全可言。
老李是一個(gè)釣魚(yú)愛(ài)好者,經(jīng)常在 我要買(51mai.com) 的網(wǎng)站上買各種釣魚(yú)的工具,并且通過(guò) 銀行(yinhang.com) 以賬號(hào)密碼的方式直接支付。
這天老李又在 51mai.com 上買了一根魚(yú)竿,輸入銀行賬號(hào)密碼支付成功后,在支付成功頁(yè)看到一個(gè)叫 釣魚(yú)(diaoyu.com) 的網(wǎng)站投放的一個(gè)"免費(fèi)領(lǐng)取魚(yú)餌"的廣告。
老李什么都沒(méi)想就點(diǎn)擊了這個(gè)廣告,跳轉(zhuǎn)到了釣魚(yú)的網(wǎng)站,殊不知這真是一個(gè) “釣魚(yú)” 網(wǎng)站,老李銀行賬戶里面錢全部被轉(zhuǎn)走了。

以上就是老李的錢被盜走的過(guò)程:
1.老李購(gòu)買魚(yú)竿,并登錄了銀行的網(wǎng)站輸入賬號(hào)密碼進(jìn)行了支付,瀏覽器在本地緩存了銀行的Cookie
2.老李點(diǎn)擊釣魚(yú)網(wǎng)站,釣魚(yú)網(wǎng)站使用老李登錄銀行之后的Cookie,偽造成自己是老李進(jìn)行了轉(zhuǎn)賬操作。
這個(gè)過(guò)程就是著名的CSRF(Cross Site Request Forgery),跨站請(qǐng)求偽造,正是由于可能存在的偽造請(qǐng)求,導(dǎo)致了瀏覽器的不安全。
那么如何防止CSRF攻擊呢,可以參考這篇文章:如何防止CSRF攻擊?
同源策略限制哪些行為
上面說(shuō)了 **同源策略 **是一個(gè)安全機(jī)制,他本質(zhì)是限制了從一個(gè)源加載的文檔或腳本如何與來(lái)自另一個(gè)源的資源進(jìn)行交互,這是一個(gè)用于隔離潛在惡意文件的重要安全機(jī)制。
隨著互聯(lián)網(wǎng)的發(fā)展,"同源策略"越來(lái)越嚴(yán)格,不僅限于Cookie的讀取。目前,如果非同源,共有三種行為受到限制。
(1) Cookie、LocalStorage 和 IndexDB 無(wú)法讀取。
(2) DOM 無(wú)法獲得。
(3) 請(qǐng)求的響應(yīng)被攔截。
雖然這些限制是必要的,但是有時(shí)很不方便,合理的用途也會(huì)受到影響,所以為了能夠獲取非“同源”的資源,就有了跨域資源共享。
跨域資源共享
看到這里你應(yīng)該明白,為什么文章開(kāi)頭的請(qǐng)求會(huì)被攔截了,原因就是請(qǐng)求的源和服務(wù)端的源不是“同源”,而服務(wù)端又沒(méi)有設(shè)置允許的跨域資源共享,所以請(qǐng)求的響應(yīng)被瀏覽器給攔截掉了。
CORS 是一個(gè) W3C 標(biāo)準(zhǔn),全稱是"跨域資源共享"(Cross Origin Resource Sharing),它允許瀏覽器向跨源服務(wù)器,發(fā)出 XMLHttpRequest 請(qǐng)求,從而克服了只能發(fā)送同源請(qǐng)求的限制。
CORS實(shí)現(xiàn)機(jī)制
那跨域資源共享機(jī)制是怎樣實(shí)現(xiàn)的呢?
當(dāng)一個(gè)資源(origin)通過(guò)腳本向另一個(gè)資源(host)發(fā)起請(qǐng)求,而被請(qǐng)求的資源(host)和請(qǐng)求源(origin)是不同的源時(shí)(協(xié)議、域名、端口不全部相同),瀏覽器就會(huì)發(fā)起一個(gè) 跨域 HTTP 請(qǐng)求 ,并且瀏覽器會(huì)自動(dòng)將當(dāng)前資源的域添加在請(qǐng)求頭中一個(gè)叫 Origin 的 Header 中。
當(dāng)然了,有三個(gè)標(biāo)簽本身就是允許跨域加載資源的:
- <img src=XXX>
- <link href=XXX>
- <script src=XXX>
比如某個(gè)網(wǎng)站的首頁(yè) http://domain-a.com/index.html 通過(guò) <img src="http://domain-b.com/image.jpg" /> 來(lái)加載其他域上的圖片,除此之外還有諸如通過(guò) CDN 節(jié)點(diǎn)引入css和js文件的方式。
出于安全原因,瀏覽器限制從腳本內(nèi)發(fā)起的跨域 HTTP 請(qǐng)求。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。 也就是說(shuō)使用這些 API 的 Web 應(yīng)用程序只能從加載應(yīng)用程序的同一個(gè)域請(qǐng)求 HTTP 資源,除非響應(yīng)報(bào)文中包含了正確 CORS 響應(yīng)頭。
通過(guò)在響應(yīng)報(bào)文中設(shè)置額外的 HTTP 響應(yīng)頭來(lái)告訴瀏覽器,運(yùn)行在某個(gè) origin 上的 Web 應(yīng)用被準(zhǔn)許訪問(wèn)來(lái)自不同源服務(wù)器上的資源,此時(shí)瀏覽器就不會(huì)將該響應(yīng)攔截掉了。
那這些額外的 HTTP 響應(yīng)頭是什么呢?
響應(yīng)頭是否必須含義Access-Control-Allow-Origin是該字段表示,服務(wù)端接收哪些來(lái)源的域的請(qǐng)求Access-Control-Allow-Credentials否是否可以向服務(wù)端發(fā)送Cookie,默認(rèn)是 falseAccess-Control-Expose-Headers否可以向請(qǐng)求額外暴露的響應(yīng)頭
其中只有 Access-Control-Allow-Origin 是必須的,該響應(yīng)頭的值可以是請(qǐng)求的 Origin 的值,也可以是 * ,表示服務(wù)端接收所有來(lái)源的請(qǐng)求。
當(dāng)瀏覽器發(fā)起 CORS 請(qǐng)求時(shí),默認(rèn)只能獲得6個(gè)響應(yīng)頭的值:
Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
如果還需要返回其他的響應(yīng)頭給前端,則可以通過(guò)在 Access-Control-Expose-Headers 中指定。
CORS的兩種請(qǐng)求類型
CORS有兩種類型的請(qǐng)求,分別是:簡(jiǎn)單請(qǐng)求(simple request)和非簡(jiǎn)單請(qǐng)求(not-so-simple request)
只要同時(shí)滿足以下兩大條件,就屬于簡(jiǎn)單請(qǐng)求。
(1) 請(qǐng)求方法是以下三種方法之一:
HEADGETPOST
(2) HTTP的頭信息不超出以下幾種字段:
AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type:只限于三個(gè)值 Application/x-www-form-urlencoded 、multipart/form-data、text/plain
凡是不同時(shí)滿足上面兩個(gè)條件,就屬于非簡(jiǎn)單請(qǐng)求,瀏覽器對(duì)這兩種請(qǐng)求的處理,是不一樣的。
為什么會(huì)有兩種不同類型的請(qǐng)求呢?
CORS 規(guī)范要求,對(duì)那些可能對(duì)服務(wù)器數(shù)據(jù)產(chǎn)生副作用的 HTTP 請(qǐng)求方法(特別是 GET 以外的 HTTP 請(qǐng)求,或者搭配某些 MIME 類型的 POST 請(qǐng)求),瀏覽器必須首先使用 OPTIONS 方法發(fā)起一個(gè)預(yù)檢請(qǐng)求(preflight request),從而獲知服務(wù)端是否允許該跨域請(qǐng)求。
服務(wù)器確認(rèn)允許之后,瀏覽器才能發(fā)起實(shí)際的 HTTP 請(qǐng)求。在預(yù)檢請(qǐng)求的返回中,服務(wù)器端也可以通知客戶端,是否需要攜帶身份憑證(包括 Cookies 和 HTTP 認(rèn)證相關(guān)的數(shù)據(jù))。
非簡(jiǎn)單請(qǐng)求就要求瀏覽器先發(fā)送一個(gè)預(yù)檢請(qǐng)求,預(yù)檢通過(guò)后再發(fā)送實(shí)際的請(qǐng)求。
怎樣實(shí)現(xiàn)CORS
知道了CORS的實(shí)現(xiàn)機(jī)制之后,我們就可以解決遇到的CORS的問(wèn)題了。
1.通過(guò)JSONP
利用 <script> 標(biāo)簽沒(méi)有跨域限制的漏洞,網(wǎng)頁(yè)可以得到從其他來(lái)源動(dòng)態(tài)產(chǎn)生的 JSON 數(shù)據(jù)。JSONP請(qǐng)求一定需要對(duì)方的服務(wù)器做支持才可以。
JSONP 和 AJAX 相同,都是客戶端向服務(wù)器端發(fā)送請(qǐng)求,從服務(wù)器端獲取數(shù)據(jù)的方式。但 AJAX 屬于同源策略,JSONP 屬于非同源策略(支持跨域請(qǐng)求)。JSONP優(yōu)點(diǎn)是簡(jiǎn)單兼容性好,可用于解決主流瀏覽器的跨域數(shù)據(jù)訪問(wèn)的問(wèn)題。缺點(diǎn)是僅支持 GET 方法具有局限性,不安全可能會(huì)遭受XSS攻擊。
2.利用反向代理服務(wù)器
同源策略是瀏覽器需要遵循的標(biāo)準(zhǔn),而如果是服務(wù)器向服務(wù)器請(qǐng)求就無(wú)需遵循同源策略
所以通過(guò)反向代理服務(wù)器可以有效的解決跨域問(wèn)題,代理服務(wù)器需要做以下幾個(gè)步驟:
1.接受客戶端的請(qǐng)求
2.將請(qǐng)求轉(zhuǎn)發(fā)給實(shí)際的服務(wù)器
3.將服務(wù)器的響應(yīng)結(jié)果返回給客戶端
Nginx就是類似的反向代理服務(wù)器,可以通過(guò)配置Nginx代理來(lái)解決跨域問(wèn)題。
3.服務(wù)端支持CORS
最安全的還是服務(wù)端來(lái)設(shè)置允許哪些來(lái)源的請(qǐng)求,即服務(wù)端在接收到請(qǐng)求之后,對(duì)允許的請(qǐng)求源設(shè)置 Access-Control-Allow-Origin 的響應(yīng)頭。
通過(guò)@CrossOrigin注解
這里以 Spring Boot 為例,可以通過(guò) @CrossOrigin 注解來(lái)指定哪些類或者方法支持跨越,如下列代碼所示:
/**
* 在類上加注解
*/
@CrossOrigin({"http://127.0.0.1:9528", "http://localhost:9528"})
@RestController
public class UserController {
}
@RestController
public class UserController {
@Resource
private UserFacade userFacade;
/**
* 在方法上加注解
*/
@GetMapping(ApiConstant.Urls.GET_USER_INFO)
@CrossOrigin({"http://127.0.0.1:9528", "http://localhost:9528"})
public PojoResult<UserDTO> getUserInfo() {
return userFacade.getUserInfo();
}
}
通過(guò)CorsRegistry設(shè)置全局跨域配置
@Configuration
@EnableWebMvc
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://127.0.0.1:9528", "http://localhost:9528");
}
}
如果你使用的是 Spring Boot,推薦的做法是只定義一個(gè) WebMvcConfigurer 的Bean:
@Configuration
public class MyConfiguration {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://127.0.0.1:9528", "http://localhost:9528");
}
};
}
}
以上兩種方式在沒(méi)有定義攔截器(Interceptor)的時(shí)候,使用一切正常,但是如果你有一個(gè)全局的攔截器用來(lái)檢測(cè)用戶的登錄態(tài),例如下面的簡(jiǎn)易代碼:
public class AuthenticationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 從 http 請(qǐng)求頭中取出 token
String token = httpServletRequest.getHeader("token");
// 檢查是否登錄
if (token == null) {
throw new InvalidTokenException(ResultCode.INVALID_TOKEN.getCode(), "登錄態(tài)失效,請(qǐng)重新登錄");
}
return true;
}
}
當(dāng)自定義攔截器返回true時(shí),一切正常,但是當(dāng)攔截器拋出異常(或者返回false)時(shí),后續(xù)的CORS設(shè)置將不會(huì)生效。
為什么攔截器拋出異常時(shí),CORS不生效呢?可以看下這個(gè)issue:
when interceptor preHandler throw exception, the cors is broken
有個(gè)人提交了一個(gè)issue,說(shuō)明如果在自定義攔截器的preHandler方法中拋出異常的話,通過(guò) CorsRegistry 設(shè)置的全局 CORS 配置就失效了,但是Spring Boot 的成員不認(rèn)為這是一個(gè)Bug。
然后提交者舉了個(gè)具體的例子:
他先定義了CorsRegistry,并添加了一個(gè)自定義的攔截器,攔截器中拋出了異常

然后他發(fā)現(xiàn)AbstractHandlerMapping在添加CorsInterceptor的時(shí)候,是將 Cors 的攔截器加在攔截器鏈的最后:

那就會(huì)造成上面說(shuō)的問(wèn)題,在自定義攔截器中拋出異常之后,CorsInterceptor 攔截器就沒(méi)有機(jī)會(huì)執(zhí)行向 response 中設(shè)置 CORS 相關(guān)響應(yīng)頭了。
issue的提交者也給出了解決的方案,就是將用來(lái)處理 Cors 的攔截器 CorsInterceptor 加在攔截器鏈的第一個(gè)位置:

這樣的話請(qǐng)求來(lái)了之后,第一個(gè)就會(huì)為 response 設(shè)置相應(yīng)的 CORS 響應(yīng)頭,后續(xù)如果其他自定義攔截器拋出異常,也不會(huì)有影響了。
感覺(jué)是一個(gè)可行的解決方案,但是 Spring Boot 的成員認(rèn)為這不是 Spring Boot 的Bug,而是 Spring Framework 的 Bug,所以將這個(gè)issue關(guān)閉了。
通過(guò)CorsFilter設(shè)置全局跨域配置
既然通過(guò)攔截器設(shè)置全局跨域配置會(huì)有問(wèn)題,那我們還有另外一種方案,通過(guò)過(guò)濾器 CorsFilter 的方式來(lái)設(shè)置,代碼如下:
@Configuration
public class MyConfiguration {
@Bean
public FilterRegistrationBean corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("http://127.0.0.1:9528");
config.addAllowedOrigin("http://localhost:9528");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
bean.setOrder(0);
return bean;
}
}
為什么過(guò)濾器可以而攔截器不行呢?
因?yàn)檫^(guò)濾器依賴于 Servlet 容器,基于函數(shù)回調(diào),它可以對(duì)幾乎所有請(qǐng)求進(jìn)行過(guò)濾。而攔截器是依賴于 Web 框架(如Spring MVC框架),基于反射通過(guò)AOP的方式實(shí)現(xiàn)的。
在觸發(fā)順序上如下圖所示:

因?yàn)檫^(guò)濾器在觸發(fā)上是先于攔截器的,但是如果有多個(gè)過(guò)濾器的話,也需要將 CorsFilter 設(shè)置為第一個(gè)過(guò)濾器才行。
原文:https://my.oschina.net/u/3216837/blog/3196454