跨域問題是怎樣形成的

跨域問題的由來

相信不少人都或多或少了解過跨域問題,尤爲在現現在先後端分離大行其道的時候。css

你在本地開發一個前端項目,這個項目是經過 node 運行的,端口是9528,而服務端是經過 spring boot 提供的,端口號是7001。html

當你調用一個服務端接口時,極可能獲得相似下面這樣的一個錯誤:前端

request-blocked-by-cors

而後你在發送請求的地方debug,在出現異常的地方你將獲得這樣的結果:java

blocked-error

異常對象很詭異,返回的 response 是 undefined 的,而且 message 消息中只有一個"Network Error"。node

看到這裏你應該要知道,你遇到跨域問題了。git

可是你須要明確的一點是,這個請求已經發出去了,服務端也接收到並處理了,可是返回的響應結果不是瀏覽器想要的結果,因此瀏覽器將這個響應的結果給攔截了,這就是爲何你看到的response是undefined。github

瀏覽器的同源策略

那瀏覽器爲何會將服務端返回的結果攔截掉呢?spring

這就須要咱們瞭解瀏覽器基於安全方面的考慮,而引入的 同源策略(same-origin policy) 了。後端

早在1995年,Netscape 公司就在瀏覽器中引入了「同源策略」。跨域

最初的 「同源策略」,主要是限制Cookie的訪問,A網頁設置的 Cookie,B網頁沒法訪問,除非B網頁和A網頁是「同源」的。

那麼怎麼肯定兩個網頁是否是「同源」呢,所謂「同源」就是指"協議+域名+端口"三者相同,即使兩個不一樣的域名指向同一個ip地址,也非同源。

the-compose-of-domain

沒有同源策略的保護

那麼爲何要作這個同源的限制呢?由於若是沒有同源策略的保護,瀏覽器將沒有任何安全可言。

老李是一個釣魚愛好者,常常在 我要買(51mai.com) 的網站上買各類釣魚的工具,而且經過 銀行(yinhang.com) 以帳號密碼的方式直接支付。

這天老李又在 51mai.com 上買了一根魚竿,輸入銀行帳號密碼支付成功後,在支付成功頁看到一個叫 釣魚(diaoyu.com) 的網站投放的一個"免費領取魚餌"的廣告。

老李什麼都沒想就點擊了這個廣告,跳轉到了釣魚的網站,卻不知這真是一個 「釣魚」 網站,老李銀行帳戶裏面錢所有被轉走了。

no-same-origin-policy

以上就是老李的錢被盜走的過程:

1.老李購買魚竿,並登陸了銀行的網站輸入帳號密碼進行了支付,瀏覽器在本地緩存了銀行的Cookie

2.老李點擊釣魚網站,釣魚網站使用老李登陸銀行以後的Cookie,僞形成本身是老李進行了轉帳操做。

這個過程就是著名的CSRF(Cross Site Request Forgery),跨站請求僞造,正是因爲可能存在的僞造請求,致使了瀏覽器的不安全。

那麼如何防止CSRF攻擊呢,能夠參考這篇文章:如何防止CSRF攻擊?

同源策略限制哪些行爲

上面說了 **同源策略 **是一個安全機制,他本質是限制了從一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互,這是一個用於隔離潛在惡意文件的重要安全機制。

隨着互聯網的發展,"同源策略"愈來愈嚴格,不只限於Cookie的讀取。目前,若是非同源,共有三種行爲受到限制。

(1) Cookie、LocalStorage 和 IndexDB 沒法讀取。

(2) DOM 沒法得到。

(3) 請求的響應被攔截。

雖然這些限制是必要的,可是有時很不方便,合理的用途也會受到影響,因此爲了可以獲取非「同源」的資源,就有了跨域資源共享。

跨域資源共享

看到這裏你應該明白,爲何文章開頭的請求會被攔截了,緣由就是請求的源和服務端的源不是「同源」,而服務端又沒有設置容許的跨域資源共享,因此請求的響應被瀏覽器給攔截掉了。

CORS 是一個 W3C 標準,全稱是"跨域資源共享"(Cross Origin Resource Sharing),它容許瀏覽器向跨源服務器,發出 XMLHttpRequest 請求,從而克服了只能發送同源請求的限制。

CORS實現機制

那跨域資源共享機制是怎樣實現的呢?

當一個資源(origin)經過腳本向另外一個資源(host)發起請求,而被請求的資源(host)和請求源(origin)是不一樣的源時(協議、域名、端口不所有相同),瀏覽器就會發起一個 跨域 HTTP 請求 ,而且瀏覽器會自動將當前資源的域添加在請求頭中一個叫 Origin 的 Header 中。

固然了,有三個標籤自己就是容許跨域加載資源的:

  • <img src=XXX>
  • <link href=XXX>
  • <script src=XXX>

好比某個網站的首頁 http://domain-a.com/index.html 經過 <img src="http://domain-b.com/image.jpg" /> 來加載其餘域上的圖片,除此以外還有諸如經過 CDN 節點引入css和js文件的方式。

出於安全緣由,瀏覽器限制從腳本內發起的跨域 HTTP 請求。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。 也就是說使用這些 API 的 Web 應用程序只能從加載應用程序的同一個域請求 HTTP 資源,除非響應報文中包含了正確 CORS 響應頭。

經過在響應報文中設置額外的 HTTP 響應頭來告訴瀏覽器,運行在某個 origin 上的 Web 應用被准許訪問來自不一樣源服務器上的資源,此時瀏覽器就不會將該響應攔截掉了。

那這些額外的 HTTP 響應頭是什麼呢?

響應頭 是否必須 含義
Access-Control-Allow-Origin 該字段表示,服務端接收哪些來源的域的請求
Access-Control-Allow-Credentials 是否能夠向服務端發送Cookie,默認是 false
Access-Control-Expose-Headers 能夠向請求額外暴露的響應頭

其中只有 Access-Control-Allow-Origin 是必須的,該響應頭的值能夠是請求的 Origin 的值,也能夠是 * ,表示服務端接收全部來源的請求。

當瀏覽器發起 CORS 請求時,默認只能得到6個響應頭的值:

Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma

若是還須要返回其餘的響應頭給前端,則能夠經過在 Access-Control-Expose-Headers 中指定。

CORS的兩種請求類型

CORS有兩種類型的請求,分別是:簡單請求(simple request)和非簡單請求(not-so-simple request)

只要同時知足如下兩大條件,就屬於簡單請求。

(1) 請求方法是如下三種方法之一:

  • HEAD
  • GET
  • POST

(2) HTTP的頭信息不超出如下幾種字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限於三個值 application/x-www-form-urlencodedmultipart/form-datatext/plain

凡是不一樣時知足上面兩個條件,就屬於非簡單請求,瀏覽器對這兩種請求的處理,是不同的。

爲何會有兩種不一樣類型的請求呢?

CORS 規範要求,對那些可能對服務器數據產生反作用的 HTTP 請求方法(特別是 GET 之外的 HTTP 請求,或者搭配某些 MIME 類型的 POST 請求),瀏覽器必須首先使用 OPTIONS 方法發起一個預檢請求(preflight request),從而獲知服務端是否容許該跨域請求。

服務器確認容許以後,瀏覽器才能發起實際的 HTTP 請求。在預檢請求的返回中,服務器端也能夠通知客戶端,是否須要攜帶身份憑證(包括 Cookies 和 HTTP 認證相關的數據)。

非簡單請求就要求瀏覽器先發送一個預檢請求,預檢經過後再發送實際的請求。

怎樣實現CORS

知道了CORS的實現機制以後,咱們就能夠解決遇到的CORS的問題了。

1.經過JSONP

利用 <script> 標籤沒有跨域限制的漏洞,網頁能夠獲得從其餘來源動態產生的 JSON 數據。JSONP請求必定須要對方的服務器作支持才能夠。

JSONP 和 AJAX 相同,都是客戶端向服務器端發送請求,從服務器端獲取數據的方式。但 AJAX 屬於同源策略,JSONP 屬於非同源策略(支持跨域請求)。JSONP優勢是簡單兼容性好,可用於解決主流瀏覽器的跨域數據訪問的問題。缺點是僅支持 GET 方法具備侷限性,不安全可能會遭受XSS攻擊。

2.利用反向代理服務器

同源策略是瀏覽器須要遵循的標準,而若是是服務器向服務器請求就無需遵循同源策略

因此經過反向代理服務器能夠有效的解決跨域問題,代理服務器須要作如下幾個步驟:

1.接受客戶端的請求

2.將請求轉發給實際的服務器

3.將服務器的響應結果返回給客戶端

Nginx就是相似的反向代理服務器,能夠經過配置Nginx代理來解決跨域問題。

3.服務端支持CORS

最安全的仍是服務端來設置容許哪些來源的請求,即服務端在接收到請求以後,對容許的請求源設置 Access-Control-Allow-Origin 的響應頭。

經過@CrossOrigin註解

這裏以 Spring Boot 爲例,能夠經過 @CrossOrigin 註解來指定哪些類或者方法支持跨越,以下列代碼所示:

/**
 * 在類上加註解
 */
@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();
    }
}

經過CorsRegistry設置全局跨域配置

@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,推薦的作法是隻定義一個 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");
            }
        };
    }
}

以上兩種方式在沒有定義攔截器(Interceptor)的時候,使用一切正常,可是若是你有一個全局的攔截器用來檢測用戶的登陸態,例以下面的簡易代碼:

public class AuthenticationInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // 從 http 請求頭中取出 token
        String token = httpServletRequest.getHeader("token");
        // 檢查是否登陸
        if (token == null) {
            throw new InvalidTokenException(ResultCode.INVALID_TOKEN.getCode(), "登陸態失效,請從新登陸");
        }
        return true;
    }
}

當自定義攔截器返回true時,一切正常,可是當攔截器拋出異常(或者返回false)時,後續的CORS設置將不會生效。

爲何攔截器拋出異常時,CORS不生效呢?能夠看下這個issue:

when interceptor preHandler throw exception, the cors is broken

有我的提交了一個issue,說明若是在自定義攔截器的preHandler方法中拋出異常的話,經過 CorsRegistry 設置的全局 CORS 配置就失效了,可是Spring Boot 的成員不認爲這是一個Bug。

而後提交者舉了個具體的例子:

他先定義了CorsRegistry,並添加了一個自定義的攔截器,攔截器中拋出了異常

cors-registry-1

而後他發現AbstractHandlerMapping在添加CorsInterceptor的時候,是將 Cors 的攔截器加在攔截器鏈的最後:

cors-registry-2

那就會形成上面說的問題,在自定義攔截器中拋出異常以後,CorsInterceptor 攔截器就沒有機會執行向 response 中設置 CORS 相關響應頭了。

issue的提交者也給出瞭解決的方案,就是將用來處理 Cors 的攔截器 CorsInterceptor 加在攔截器鏈的第一個位置:

cors-registry-3

這樣的話請求來了以後,第一個就會爲 response 設置相應的 CORS 響應頭,後續若是其餘自定義攔截器拋出異常,也不會有影響了。

感受是一個可行的解決方案,可是 Spring Boot 的成員認爲這不是 Spring Boot 的Bug,而是 Spring Framework 的 Bug,因此將這個issue關閉了。

經過CorsFilter設置全局跨域配置

既然經過攔截器設置全局跨域配置會有問題,那咱們還有另一種方案,經過過濾器 CorsFilter 的方式來設置,代碼以下:

@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;
	}
}

爲何過濾器能夠而攔截器不行呢?

由於過濾器依賴於 Servlet 容器,基於函數回調,它能夠對幾乎全部請求進行過濾。而攔截器是依賴於 Web 框架(如Spring MVC框架),基於反射經過AOP的方式實現的。

在觸發順序上以下圖所示:

filter-interceptor-order

由於過濾器在觸發上是先於攔截器的,可是若是有多個過濾器的話,也須要將 CorsFilter 設置爲第一個過濾器才行。

參考文獻

瀏覽器的同源策略

瀏覽器同源政策及其規避方法

HTTP訪問控制(CORS)

跨域資源共享 CORS 詳解

如何防止CSRF攻擊?

逐碼,專一於原創分享,用通俗易懂的圖文描述源碼及原理

CodeChaaaser

相關文章
相關標籤/搜索