Spring Boot 2.x (三): 跨域處理方案之 Cor

  • 2019 年 11 月 5 日
  • 筆記

一、什麼是跨域

1.1 URI 文法

URI 文法由 URI 協議名(例如 「http」,「ftp」,「mailto」 或 「file」),一個冒號,和協議對應的內容所構成。特定的協議定義了協議內容的語法和語義,而所有的協議都必須遵循一定的 URI 文法通用規則,亦即為某些專門目的保留部分特殊字元。

下面展示了 URI 例子及它們的組成部分:

                     許可權                 路徑        ┌───────────────┴───────────────┐┌───┴────┐  abc://username:[email protected]:123/path/data?key=value&key2=value2#fragid1  └┬┘   └───────┬───────┘ └────┬────┘ └┬┘           └─────────┬─────────┘ └──┬──┘  協議        用戶資訊         主機名    埠                  查詢參數          片段

URL 是一種 URI,它標識一個互聯網資源,並指定對其進行操作或獲取該資源的方法。

1.2 瀏覽器的同源策略

同源策略限制了從同一個源載入的文檔或腳本如何與來自另一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。如果兩個頁面的協議,埠(如果有指定)和主機名都相同,則兩個頁面具有相同的源。只要協議,主機名,埠這三項組成部分中有一項不同,就可以認為是不同的域,不同的域之間互相訪問資源,就被稱之為跨域。

下表給出了相對 http://store.company.com/dir/page.html 同源檢測的示例:

URL

結果

原因

http://store.company.com/dir2/other.html

成功

只有路徑不同

http://store.company.com/dir/inner/another.html

成功

只有路徑不同

https://store.company.com/secure.html

失敗

不同協議 ( https和http )

http://store.company.com:81/dir/etc.html

失敗

不同埠 ( http:// 80 是默認埠)

http://news.company.com/dir/other.html

失敗

不同域名 ( news 和 store )

同源策略會限制以下幾種行為:

  • Cookie、LocalStorage 和 IndexDB 無法讀取;
  • DOM 和 JS 對象無法獲得;
  • AJAX 請求不能發送,被瀏覽器攔截了。

在前後端分離的項目中,因為前端站點和後台站點一般是分開部署的,所以在實際開發過程中也會出現跨域問題。當然遇到問題最終還是要解決的,下面我們來看一下跨域問題的一些解決方案。

二、如何解決跨域

2.1 跨域解決方案

針對同源策略限制而引起的跨域問題,有以下 9 種解決方案:

  1. JSONP 跨域
  2. 跨域資源共享(CORS)
  3. Nginx 反向代理
  4. Node.js 中間件代理
  5. document.domain + iframe
  6. location.hash + iframe 跨域
  7. window.name + iframe 跨域
  8. postMessage 跨域
  9. WebSocket 協議跨域

接下來我們將著重介紹 CORS 解決方案,因為它是解決 AJAX 請求跨域問題的一劑「良藥」,對其它方案感興趣的同學請自行查閱相關資料。

2.2 CORS 簡介

跨域資源共享(CORS)是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器讓運行在一個域上的 Web 應用被允許訪問來自不同源伺服器上的指定的資源。CORS 需要瀏覽器和伺服器同時支援。目前,所有主流的瀏覽器都支援該功能。整個 CORS 通訊過程,都是瀏覽器自動完成,不需要用戶參與。

對於開發者來說,CORS 通訊與同源的 AJAX 通訊沒有差別,程式碼完全一樣。瀏覽器一旦發現 AJAX 請求跨源,就會自動添加一些附加的頭資訊,有時還會多出一次附加的請求(預檢請求),但用戶不會有感覺。實現 CORS 通訊的關鍵是後端,只要後端根據實際情況設置相應的響應頭資訊,就能解決 AJAX 請求跨域問題。

前面我們已經介紹跨域的概念和跨域問題的一些解決方案,現在我們進入本文的正題 —— Spring Boot CORS 跨域處理。

三、Spring Boot 環境搭建

本項目所使用的開發環境及主要框架版本:

  • java version 「1.8.0_144」
  • spring boot 2.2.0.RELEASE

首先新建一個 Spring Boot 項目,然後在根目錄下的 pom.xml 文件中引入以下依賴:

<dependencies>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-thymeleaf</artifactId>          </dependency>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-web</artifactId>          </dependency>  </dependencies>

接著新建一個 index.html 頁面並輸入以下程式碼:

<!DOCTYPE html>  <html lang="zh">  <head>      <meta charset="UTF-8">      <title>Semlinker's Springboot2 Cors</title>  </head>  <body>  <h3>Semlinker's Springboot2 Cors</h3>  <div>    用戶列表:    <p id="users"></p>  </div>  <script>      (function () {          var xmlhttp = new XMLHttpRequest();          xmlhttp.onreadystatechange = function () {              if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {                  document.querySelector("#users").innerHTML = xmlhttp.responseText;              }          };          xmlhttp.open("GET", "http://localhost:8081/users");          xmlhttp.send();      })();  </script>  </body>  </html>

以上程式碼比較簡單,就是創建 XMLHttpRequest 對象,然後往 http://localhost:8081/users 地址發送 GET 請求。下面我們來創建一個 HomeController,用於處理 //users 請求,該控制器的定義如下:

@Controller  public class HomeController {      private String[] users = {"Semlinker", "Lolo", "Kakuqo"};        @GetMapping("/users")      @ResponseBody      public String[] users() {          return users;      }        @GetMapping("/")      public String index() {          return "index";      }  }

萬事俱備只欠東風,最後我們還需要配置一下 Idea 的 Run/Debug Configurations,下圖的重點是通過 Program arguments 參數配置 CorsApp-8080 應用程式的埠,即 --server.port=8080。同理,我們通過設置不同的應用程式埠,就可以啟動另一個新的應用程式,即 Cors-8081應用。

在配置完成後,分別啟動 CorsApp-8080 和 CorsApp-8081 兩個應用程式,待兩個應用啟動完成後,訪問 http://localhost:8080/ 地址,此時你會發現頁面上並沒有顯示任何用戶。而訪問 http://localhost:8081/ 地址時,你確可以看到以下內容:

Semlinker's Springboot2 Cors  用戶列表:  ["Semlinker","Lolo","Kakuqo"]

接著我們再次訪問 http://localhost:8080/ 地址,然後打開控制台,這時你會看到以下錯誤資訊:

(index):1 Access to XMLHttpRequest at 'http://localhost:8081/users' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

『http://localhost:8080' has been blocked by CORS policy 這行消息告訴了我們具體原因,很明顯是由於埠不同(8080 -> 8081)違反了同源策略,瀏覽器出於安全考慮限制了跨域請求。現在我們也遇到跨域問題,下面我們就來學習一下在 Spring Boot 中如何利用 Cors 來解決上述的 AJAX 請求跨域問題。

四、Spring Boot Cors 跨域解決方案

4.1 CrossOrigin 註解

在 Spring Boot 中為我們提供了一個註解 @CrossOrigin 來實現跨域,這個註解可以實現方法級別的細粒度的跨域控制。

@Target({ElementType.TYPE, ElementType.METHOD})  @Retention(RetentionPolicy.RUNTIME)  @Documented  public @interface CrossOrigin {}

由上可知我們可以在類或者方法上使用該註解,如果在類上添加該註解,該類下的所有介面都允許跨域訪問,如果在方法上添加註解,那麼僅限於添加註解的方法可以訪問。在該註解中包含以下屬性:

@Target({ElementType.TYPE, ElementType.METHOD})  @Retention(RetentionPolicy.RUNTIME)  @Documented  public @interface CrossOrigin {  	@AliasFor("origins")  	String[] value() default {};    	@AliasFor("value")  	String[] origins() default {};    	String[] allowedHeaders() default {};    	String[] exposedHeaders() default {};    	RequestMethod[] methods() default {};    	String allowCredentials() default "";    	long maxAge() default -1;  }

CrossOrigin 註解每個屬性的詳細含義如下所示:

屬性

含義

value

指定所支援域的集合, 表示所有域都支援,默認值為 。這些值對應於 HTTP 請求頭中的 Access-Control-Allow-Origin

origins

@AliasFor(「value」),與 value 屬性一樣

allowedHeaders

允許請求頭中的 headers,在預檢請求 Access-Control-Allow-Headers 響應頭中展示

exposedHeaders

響應頭中允許訪問的 headers,在實際請求的 Access-Control-Expose-Headers 響應頭中展示

methods

支援的 HTTP 請求方法列表,默認和 Controller 中的方法上標註的一致。

allowCredentials

表示瀏覽器在跨域請求中是否攜帶憑證,比如 cookies。在預檢請求的 Access-Control-Allow-Credentials 響應頭中展示

maxAge

預檢請求響應的最大快取時間,單位為秒。在預檢請求的 Access-Control-Max-Age 響應頭中展示

介紹完 @CrossOrigin 註解的相關知識,我們來修改一下 HomeController 控制器,在 users 方法上添加 @CrossOrigin 註解:

@Controller  public class HomeController {      private String[] users = {"Semlinker", "Lolo", "Kakuqo"};        @GetMapping("/users")      @ResponseBody      @CrossOrigin      public String[] users() {          return users;      }  }

完成修改之後,重新啟動一下項目,然後繼續訪問 http://localhost:8080/ 地址,如果一切順利的話,在頁面就可以看到期望的內容:

Semlinker's Springboot2 Cors  用戶列表:  ["Semlinker","Lolo","Kakuqo"]

現在通過瀏覽器的開發者工具,查看 http://localhost:8081/users 的 HTTP 請求報文:

從圖中可知,當 users 方法添加了 @CrossOrigin 註解之後,響應頭返回了 Access-Control-Allow-Origin:*

資訊。

4.2 實現 WebMvcConfigurer 介面

除了使用 @CrossOrigin 註解外,我們還可以通過實現 WebMvcConfigurer 介面來實現統一的跨域配置。首先在當前項目中新建一個 config 包,接著創建一個 CorsConfiguration 配置類,該類需要實現 WebMvcConfigurer 介面,然後覆寫 addCorsMappings 方法,最後利用 CorsRegistry 對象進行跨域配置,具體實現如下所示:

// com/semlinker/config/CorsConfig.java  @Configuration  public class CorsConfig implements WebMvcConfigurer {      @Override      public void addCorsMappings(CorsRegistry registry) {          registry.addMapping("/**")                  .allowedOrigins("*")                  .allowedMethods("GET");      }  }

配置完成之後,為了排除 @CrossOrigin 註解的影響,我們需要先移除 HomeController 類的 users 方法上的 @CrossOrigin 註解,重啟項目再次訪問 http://localhost:8080/ 地址,發現效果一樣。

4.3 過濾器

過濾器是向 Web 應用程式的請求和響應,添加相關功能的 Web 服務組件。過濾器會攔截用戶發送至 Web 資源伺服器的請求,處理後將請求資訊傳遞給 Web 資源伺服器。Web 資源伺服器的響應也會經過過濾器處理後,再返回給用戶。因此我們就可以利用過濾器的特性來統一添加跨域響應頭。

這裡我們可以直接利用 org.springframework.web.filter 包下的 CorsFilter 過濾器而不用自己實現 Cors 過濾器,有了過濾器後,還需要對它進行註冊,註冊方式如下:

// com/semlinker/config/CorsConfig.java  @Configuration  public class CorsConfig implements WebMvcConfigurer {      @Bean      public FilterRegistrationBean corsFilter() {          UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();          CorsConfiguration config = new CorsConfiguration();          config.setAllowCredentials(true);          config.addAllowedOrigin("*");          source.registerCorsConfiguration("/**", config);          FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));          bean.setOrder(0);          return bean;      }  }

項目地址:https://github.com/semlinker/springstack/tree/master/springboot2-cors

五、參考資源

  • Wikipedia – 統一資源標誌符
  • MDN – Same-origin_policy
  • MrBird – Spring Boot 中處理跨域