跨域問題是怎樣造成的

跨域問題的由來

相信很多人都或多或少了解過跨域問題,尤其在現如今前後端分離大行其道的時候。

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

當你調用一個服務端接口時,很可能得到類似下面這樣的一個錯誤:

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

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

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

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

瀏覽器的同源策略

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

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

早在 1995 年,Netscape 公司就在瀏覽器中引入了 “同源策略”。

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

那麼怎麼確定兩個網頁是不是 “同源” 呢,所謂 “同源” 就是指 "協議 + 域名 + 端口" 三者相同,即便兩個不同的域名指向同一個 ip 地址,也非同源。

沒有同源策略的保護

那麼爲什麼要做這個同源的限制呢?因爲如果沒有同源策略的保護,瀏覽器將沒有任何安全可言。

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

老李什麼都沒想就點擊了這個廣告,跳轉到了釣魚的網站,殊不知這真是一個 “釣魚” 網站,老李銀行賬戶裏面錢全部被轉走了。

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

  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 通過 來加載其他域上的圖片,除此之外還有諸如通過 CDN 節點引入 css 和 js 文件的方式。

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

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

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

其中只有 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-urlencoded 、multipart/form-datatext/plain

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

爲什麼會有兩種不同類型的請求呢?

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

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

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

怎樣實現 CORS

知道了 CORS 的實現機制之後,我們就可以解決遇到的 CORS 的問題了。

1. 通過 JSONP

利用 標籤沒有跨域限制的漏洞,網頁可以得到從其他來源動態產生的 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,並添加了一個自定義的攔截器,攔截器中拋出了異常

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

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

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

這樣的話請求來了之後,第一個就會爲 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 的方式實現的。

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

因爲過濾器在觸發上是先於攔截器的,但是如果有多個過濾器的話,也需要將 CorsFilter 設置爲第一個過濾器纔行。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/7LZqwkXKC_Xi2XZlqv3qFw