java springmvc 前端 跨域問題


有個朋友在寫扇貝插件的時候遇到了跨域問題。
於是我對解決跨域問題的方式進行了一番探討。

問題

API:查詢單詞
URL: https://api.shanbay.com/bdc/search/?word={word}
請求方式: GET
參數: {word}, 必須,要查詢的單詞

報錯為

XMLHttpRequest cannot load http://localhost/home/saveCandidate. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. The response had HTTP status code 404.

這就是典型的跨域問題。

但是我在瀏覽器里輸入URL是可以進行查詢單詞的操作的,有什么不同,即下面兩個問題

  1. 為什么在瀏覽器地址欄輸入URL不會出現跨域問題。
  2. 不在服務器運行的html是否可以完成一次http請求

經過Google和自己測試

  1. 跨域限制是瀏覽器行為,不是服務器行為。 瀏覽器認為地址欄輸入時安全的,所以不限制認為是跨域。
  2. 可以,只要服務器配置為所有域都可以進行請求,那么不在服務器運行的HTML就可以完成http請求。

什么是跨域問題

同源策略

同源指的是域名(或IP)協議端口都相同,不同源的客戶端腳本(javascript、ActionScript)在沒明確授權的情況下,不能讀寫對方的資源。

URL 解釋 是否跨域
http://www.morethink.cn 原來的URL  
http://www.image.morethink.cn 子域名 跨域(cookie也無法訪問)
http://morethink.cn 不加www 跨域
https://www.morethink.cn 更改協議 跨域
http://www.morethink.cn:8080 更改端口號 跨域

原因

同源政策的目的,是為了保證用戶信息的安全,防止惡意的網站竊取數據。
設想這樣一種情況:A網站是一家銀行,用戶登錄以后,又去瀏覽其他網站。如果其他網站可以讀取A網站的Cookie,會發生什么?
很顯然,如果Cookie包含隱私(比如存款總額),這些信息就會泄漏。更可怕的是,Cookie往往用來保存用戶的登錄狀態,如果用戶沒有退出登錄,其他網站就可以冒充用戶,為所欲為。因為瀏覽器同時還規定,提交表單不受同源政策的限制。
由此可見,"同源政策"是必需的,否則 Cookie 可以共享,互聯網就毫無安全可言了。

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

  1. Cookie、LocalStorage 和 IndexDB 無法讀取
  2. DOM 和 Js對象無法獲得
  3. AJAX 請求不能發送

模擬跨域問題

測試URL為 http://localhost:80/home/allProductions

可以直接在瀏覽器console中執行

var xhr = new XMLHttpRequest(); xhr.open('GET', 'http://localhost:80/home/allProductions',true); xhr.send(); xhr.onreadystatechange=function() { if(xhr.readyState == 4) { if(xhr.status == 200) { console.log(JSON.parse(xhr.responseText)); } } }

在任意網站打開控制台,執行此段代碼可以模擬跨域請求。

在知乎控制台打開報錯如下

Mixed Content: The page at 'https://www.zhihu.com/question/26376773' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://localhost/home/allProductions'. This request has been blocked; the content must be served over HTTPS.

因為知乎是https,報錯與普通的http協議不同。

再澄清一下跨域問題:

  1. 並非瀏覽器限制了發起跨站請求,而是跨站請求可以正常發起,但是返回結果被瀏覽器攔截了。最好的例子是CRSF跨站攻擊原理,無論是否跨域,請求已經發送到了后端服務器!
  2. 但是,有些瀏覽器不允許從HTTPS的域跨域訪問HTTP,比如Chrome和Firefox,這些瀏覽器在請求還未發出的時候就會攔截請求,這是一個特例。

在博客園控制台打開報錯如下

XMLHttpRequest cannot load http://localhost/home/allProductions. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://www.cnblogs.com' is therefore not allowed access.

怎么解決跨域問題

解決方案有很多

  1. 通過jsonp跨域
  2. document.domain + iframe跨域
  3. location.hash + iframe
  4. window.name + iframe跨域
  5. postMessage跨域
  6. 跨域資源共享(CORS)
  7. 前端通過Nginx解決跨域問題
  8. nodejs中間件代理跨域
  9. WebSocket協議跨域

這里主要介紹SpringMVC解決跨域問題的方式。

  1. JSONP
  2. CORS
  3. WebSocket

JSONP

可以直接參考Spring MVC 4.1 支持jsonp進行配置你的SpringMVC注解

JSONP 原理

我雖然請求不了json數據,但是我可以請求一個Content-Typeapplication/javascript的JavaScript對象,這樣就可以避免瀏覽器的同源策略。

就是當服務器接受到名為jsonp或者callback的參數時,返回Content-Type: application/javascript的結果,從而避免瀏覽器的同源策略檢測。

  1. 在控制台中直接進行測試你的jsonp是否配置成功
            function println(data) { console.log(data); } var url = "http://localhost:80/home/allProductions?&callback=println"; // 創建script標簽,設置其屬性 var script = document.createElement('script'); script.setAttribute('src', url); // 把script標簽加入head,此時調用開始 document.getElementsByTagName('head')[0].appendChild(script);

  1. 使用JQuery測試你的jsonp是否配置成功(需要自己添加jQuery.js)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script type="text/javascript" src="js/jquery.min.js"></script> <script type="text/javascript"> function println(data) { console.log(data); console.log('print'); } function jsonp_test() { $.ajax({ type: "get", url: "http://localhost:80/home/allProductions", dataType: "jsonp", jsonp: "callback",//傳遞給請求處理程序或頁面的,用以獲得jsonp回調函數名的參數名(一般默認為:callback) jsonpCallback: "println", //返回后調用的處理函數 error: function () { //請求出錯的處理 alert("請求出錯"); } }); } </script> </head> <body onload="jsonp_test()"> </body> </html>

CORS

CORS是一個W3C標准,全稱是"跨域資源共享"(Cross-origin resource sharing)。
它允許瀏覽器向跨源服務器,發出XMLHttpRequest請求,從而克服了AJAX只能同源使用的限制。

CORS需要瀏覽器和服務器同時支持。

  1. 所有瀏覽器都支持該功能,IE瀏覽器不能低於IE10。
    整個CORS通信過程,都是瀏覽器自動完成,不需要用戶參與。 對於開發者來說,CORS通信與同源的AJAX通信沒有差別,代碼完全一樣。瀏覽器一旦發現AJAX請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。
  2. 實現CORS通信的關鍵是服務器。只要服務器實現了CORS接口,就可以跨源通信。

即CORS與普通請求代碼一樣。

CORS與JSONP相比

  1. JSONP只能實現GET請求,而CORS支持所有類型的HTTP請求。
  2. 使用CORS,開發者可以使用普通的XMLHttpRequest發起請求和獲得數據,比起JSONP有更好的錯誤處理。
  3. JSONP主要被老的瀏覽器支持,它們往往不支持CORS,而絕大多數現代瀏覽器都已經支持了CORS。

@CrossOrigin注解

此注解既可用於方法也可用於類

源碼如下:

    @CrossOrigin(origins = "http://www.zhihu.com") @RequestMapping(value = "/allProductions", method = RequestMethod.GET) public Result getAllOldProductions() { }

@CrossOrigin注解既可注解在方法上,也可注解在類上。

完成配置之后

XML全局配置

所有跨域請求都可以訪問

<mvc:cors> <mvc:mapping path="/**" /> </mvc:cors>

更加細粒度的配置:

<mvc:cors> <mvc:mapping path="/api/**" allowed-origins="http://domain1.com, http://domain2.com" allowed-methods="GET, PUT" allowed-headers="header1, header2, header3" exposed-headers="header1, header2" allow-credentials="false" max-age="123" /> <mvc:mapping path="/resources/**" allowed-origins="http://domain1.com" /> </mvc:cors>

WebSocket

WebSocket是一種通信協議,使用ws://(非加密)和wss://(加密)作為協議前綴,在2008年誕生,2011年成為國際標准。所有瀏覽器都已經支持了

它的最大特點就是,服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息,是真正的雙向平等對話,屬於服務器推送技術的一種。

該協議不實行同源政策,只要服務器支持,就可以通過它進行跨源通信

請求頭信息:(多了個 origin)

GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw== Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com

響應頭:(如果origin在白名單內)

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat

相比於HTTP/2

HTTP/2只是對HTML、CSS等JS資源的傳輸方式進行了優化,並沒有提供新的JS API,不能用於實時傳輸消息,也無法推送指定的信息。

參考文檔

  1. 跨域
  2. SpringMVC 跨域解決方法
  3. 前端常見跨域解決方案(全)

 

1. CORS 簡介

同源策略(same origin policy)是瀏覽器安全的基石。在同源策略的限制下,非同源的網站之間不能發送 ajax 請求的。

為了解決這個問題,w3c 提出了跨源資源共享,即 CORS(Cross-Origin Resource Sharing)。

CORS 做到了兩點:

  1. 不破壞即有規則
  2. 服務器實現了 CORS 接口,就可以跨源通信

基於這兩點,CORS 將請求分為兩類:簡單請求和非簡單請求。

1.1 簡單請求

可以先看下 CORS 出現前的情況:跨源時能夠通過 script 或者 image 標簽觸發 GET 請求或通過表單發送一條 POST 請求,但這兩種請求 HTTP 頭信息中都不能包含任何自定義字段。

簡單請求對應該規則,因此對簡單請求的定義為:

請求方法是 HEADGET 或 POST 且 HTTP 頭信息不超過以下幾個字段:AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type(只限於 application/x-www-form-urlencodedmultipart/form-datatext/plain)。

比如有一個簡單請求:

  1.  
    GET / test HTTP/1.1
  2.  
    Accept: */*
  3.  
    Accept-Encoding: gzip, deflate, sdch, br
  4.  
    Origin: http://www.examples.com
  5.  
    Host: www.examples.com

對於這樣的簡單請求,CORS 的策略是請求時,**在頭信息中添加一個 Origin 字段**,服務器收到請求后,根據該字段判斷是否允許該請求。

  1. 如果允許,則在 HTTP 頭信息中添加 Access-Control-Allow-Origin 字段,並返回正確的結果
  2. 如果不允許,則不在頭信息中添加 Access-Control-Allow-Origin 字段。

瀏覽器先於用戶得到返回結果,根據有無 Access-Control-Allow-Origin 字段來決定是否攔截該返回結果。

對於 CORS 出現前的一些服務,CORS 對他們的影響分兩種情況:

  1. script 或者 image 觸發的 GET 請求不包含 Origin 頭,所以不受到 CORS 的限制,依舊可用。
  2. 如果是 ajax 請求,HTTP 頭信息中會包含 Origin 字段,由於服務器沒有做任何配置,所以返回結果不會包含 Access-Control-Allow-Origin,因此返回結果會被瀏覽器攔截,接口依舊不可以被 ajax 跨源訪問。

可以看出,CORS 的出現,沒有對”舊的“服務造成任何影響。

另外,除了提到的 Access-Control-Allow-Origin 還有幾個字段用於描述 CORS 返回結果:

  1. Access-Control-Allow-Credentials: 可選,用戶是否可以發送、處理 cookie。
  2. Access-Control-Expose-Headers:可選,可以讓用戶拿到的字段。有幾個字段無論設置與否都可以拿到的,包括:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。

1.2 非簡單請求

除了簡單請求之外的請求,就是非簡單請求。

對於非簡單請求的跨源請求,**瀏覽器會在真實請求發出前**,增加一次 OPTION 請求,稱為預檢請求(preflight request)。預檢請求將真實請求的信息,包括請求方法、自定義頭字段、源信息添加到 HTTP 頭信息字段中,詢問服務器是否允許這樣的操作。

比如對於 DELETE 請求:

  1.  
    OPTIONS /test HTTP/1.1
  2.  
    Origin: http://www.examples.com
  3.  
    Access-Control-Request-Method: DELETE
  4.  
    Access-Control-Request-Headers: X-Custom-Header
  5.  
    Host: www.examples.com

與 CORS 相關的字段有:

  1. Access-Control-Request-Method: 真實請求使用的 HTTP 方法。
  2. Access-Control-Request-Headers: 真實請求中包含的自定義頭字段。

服務器收到請求時,需要分別對 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 進行驗證,驗證通過后,會在返回 Http 頭信息中添加

  1.  
    Access-Control-Allow-Origin: http://www.examples.com
  2.  
    Access-Control-Allow-Methods: GET, POST, PUT, DELETE
  3.  
    Access-Control-Allow-Headers: X-Custom-Header
  4.  
    Access-Control-Allow-Credentials: true
  5.  
    Access-Control-Max-Age: 1728000

他們的含義分別是:

  1. Access-Control-Allow-Methods: 真實請求允許的方法
  2. Access-Control-Allow-Headers: 服務器允許使用的字段
  3. Access-Control-Allow-Credentials: 是否允許用戶發送、處理 cookie
  4. Access-Control-Max-Age: 預檢請求的有效期,單位為秒。有效期內,不會重復發送預檢請求

當預檢請求通過后,瀏覽器會發送真實請求到服務器。這就實現了跨源請求。

了解完 CORS,接下來我們來搭建簡單的 Spring MVC 服務,並進一步了解 Spring MVC 如何配置 CORS。

2. Spring MVC 環境搭建

打開 http://start.spring.io/,添加 Web Dependency,然后選擇 Generate Project,下載 zip 文件,就得到了一個 spring boot demo。

圖片

解壓 zip 文件,雙擊 pom.xml 打開或用 IDEA、Eclipse 將項目按照 maven 導入。

根據 Group、Artifact、Dependencies 填寫的不同,項目目錄結構可能有些許差別,我的項目結構如下:

  1.  
    ├── src
  2.  
    │ ├── main/java
  3.  
    | └── net/xiayule/spring/cors
  4.  
    │ | └── SpringBootCorsTestApplication.java
  5.  
    | └── resources
  6.  
    | ├── static
  7.  
    | ├── templates
  8.  
    | └── application.properties
  9.  
    |
  10.  
    └── pom.xml
  11.  
     

我們需要關心的只有 SpringBootCorsTestApplication.java。在 SpringBootCorsTestApplication.java 添加以下代碼:

  1.  
    @RestController
  2.  
    @SpringBootApplication
  3.  
    public class SpringBootCorsTestApplication {
  4.  
     
  5.  
    @RequestMapping(value = "/test")
  6.  
    public String greetings() {
  7.  
    return "{\"project\":\"just a test\"}";
  8.  
    }
  9.  
     
  10.  
    public static void main(String[] args) {
  11.  
    SpringApplication.run(SpringBootCorsTestApplication.class, args);
  12.  
    }
  13.  
    }

@RequestMapping(value = "/test") 的含義是該方法接受來自 /test 的請求,並且提供了對 HTTP 的 GET、POST、DELETE 等方法的支持。

運行項目的方法為,在項目根目錄執行 mvn spring-boot:run 啟動應用,打開瀏覽器訪問 http://localhost:8080 即可看到效果:

圖片

后端服務搭建好了,接着實現前端。為了簡單,直接創建一個 test.html 文件,添加以下內容:

  1.  
    <!DOCTYPE html>
  2.  
    <html>
  3.  
    <head>
  4.  
    <title>Hello CORS</title>
  5.  
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
  6.  
     
  7.  
    <script>
  8.  
    $(document).ready(function() {
  9.  
    $.ajax({
  10.  
    url: "http://localhost:8080/test",
  11.  
    method: "POST",
  12.  
    contentType: "application/json; charset=utf-8"
  13.  
    }).then(function(data, status, jqxhr) {
  14.  
    alert(data)
  15.  
    });
  16.  
    });
  17.  
    </script>
  18.  
    </head>
  19.  
    <body>
  20.  
    </body>
  21.  
    </html>

使用瀏覽器打開該文件,就會觸發一條向 http://localhost:8080 的請求。可以通過修改上面代碼的 method,來觸發不同類型的請求。

由於是直接使用瀏覽器打開,**網頁的源為 null**, 當向源 http://localhost:8080 請求時,就變成了跨源請求,因此如果后端不加 CORS 的配置,返回的 HTTP 頭信息中不會包含 Access-Control-Allow-Origin,因此瀏覽器會報出如下錯誤:

圖片

3. 配置 CORS

一個應用可能會有多個 CORS 配置,並且可以設置每個 CORS 配置針對一個接口或一系列接口或者對所有接口生效。

舉例來說,我們需要:

  1. 讓 /test 接口支持跨源訪問,而 /test/1 或 /api 等其它接口不支持跨源訪問
  2. 讓 /test/* 這一類接口支持跨源訪問,而 /api 等其它接口不支持跨源訪問
  3. 站點所有的接口都支持跨源訪問

對第一種情況,如果想要對某一接口配置 CORS,可以在方法上添加 CrossOrigin 注解:

  1.  
    @CrossOrigin(origins = {"http://localhost:9000", "null"})
  2.  
    @RequestMapping(value = "/test", method = RequestMethod.GET)
  3.  
    public String greetings() {
  4.  
    return "{\"project\":\"just a test\"}";
  5.  
    }

第二種情況,如果想對一系列接口添加 CORS 配置,可以在類上添加注解,對該類聲明所有接口都有效:

  1.  
    CrossOrigin(origins = { "http://localhost:9000", "null"})
  2.  
    @RestController
  3.  
    @SpringBootApplication
  4.  
    public class SpringBootCorsTestApplication {
  5.  
    // xxx
  6.  
    }

第三種情況,添加全局配置,則需要添加一個配置類:

  1.  
    @Configuration
  2.  
    public class WebConfig extends WebMvcConfigurerAdapter {
  3.  
     
  4.  
    @Override
  5.  
    public void addCorsMappings(CorsRegistry registry) {
  6.  
    registry.addMapping("/**")
  7.  
    .allowedOrigins("http://localhost:9000", "null")
  8.  
    .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
  9.  
    .maxAge(3600)
  10.  
    .allowCredentials(true);
  11.  
    }
  12.  
    }

另外,還可以通過添加 Filter 的方式,配置 CORS 規則,並手動指定對哪些接口有效。

  1.  
    @Bean
  2.  
    public FilterRegistrationBean corsFilter() {
  3.  
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  4.  
    CorsConfiguration config = new CorsConfiguration();
  5.  
    config.setAllowCredentials(true); config.addAllowedOrigin("http://localhost:9000");
  6.  
    config.addAllowedOrigin("null");
  7.  
    config.addAllowedHeader("*");
  8.  
    config.addAllowedMethod("*");
  9.  
    source.registerCorsConfiguration("/**", config); // CORS 配置對所有接口都有效
  10.  
    FilterRegistrationBean bean = newFilterRegistrationBean(new CorsFilter(source));
  11.  
    bean.setOrder(0);
  12.  
    return bean;
  13.  
    }

4. 實現剖析

無論是通過哪種方式配置 CORS,其實都是在構造 CorsConfiguration。
一個 CORS 配置用一個 CorsConfiguration 類來表示,它的定義如下:

  1.  
    public class CorsConfiguration {
  2.  
    private List<String> allowedOrigins;
  3.  
    private List<String> allowedMethods;
  4.  
    private List<String> allowedHeaders;
  5.  
    private List<String> exposedHeaders;
  6.  
    private Boolean allowCredentials;
  7.  
    private Long maxAge;
  8.  
    }

Spring MVC 中對 CORS 規則的校驗,都是通過委托給 DefaultCorsProcessor 實現的。

DefaultCorsProcessor 處理過程如下:

  1. 判斷依據是 Header 中是否包含 Origin。如果包含則說明為 CORS 請求,轉到 2;否則,說明不是 CORS 請求,不作任何處理。
  2. 判斷 response 的 Header 是否已經包含 Access-Control-Allow-Origin,如果包含,證明已經被處理過了, 轉到 3,否則不再處理。
  3. 判斷是否同源,如果是則轉交給負責該請求的類處理
  4. 是否配置了 CORS 規則,如果沒有配置,且是預檢請求,則拒絕該請求,如果沒有配置,且不是預檢請求,則交給負責該請求的類處理。如果配置了,則對該請求進行校驗。

校驗就是根據 CorsConfiguration 這個類的配置進行判斷:

  1. 判斷 origin 是否合法
  2. 判斷 method 是否合法
  3. 判斷 header 是否合法
  4. 如果全部合法,則在 response header 中添加響應的字段,並交給負責該請求的類處理,如果不合法,則拒絕該請求。

5. 總結

本文介紹了 CORS 的知識以及如何在 Spring MVC 中配置 CORS。

 

最終在項目中采用springmvc的cors 實現了跨域

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM