本文主要介紹SpringBoot—CORS跨域問題詳解和解決方案。
關注微信公衆号:CodingTechWork,一起學習進步。
引言
在前後端開發過程中,遇到過一種錯誤,類似于報錯:
Access to XMLHttpRequest at 'http://127.0.0.1:8080/' from origin 'null' has been blocked by CORS policy! No 'Access-Control-Allow-Origin' header is present on the requested resource.
亦或是
XMLHttpRequest cannot load http://127.0.0.1:8080/xxx/yy/list. Response to preflight request doesn't paas access control check: No 'Access-Control-Allow-Origin' header is present on the requestesd resource. Oirgin 'http://127.0.0.1:8010' is therefore not allowed access. The response bad HTTP status code 404.
對于上述的報錯,似曾相識,這就是跨域報錯,那什麼是跨域?在了解跨域問題之前,我們還得了解一下什麼是同源政策?什麼是CORS?SpringBoot中如何解決跨域問題?
同源政策
非同源隐患
使用者登入網站A後,又通過網站A通路了網站B,若網站B可以讀取網站A的cookie(包含了網站A的使用者登入資訊、狀态等隐私資料),網站B就可以冒充使用者進行網站A的資料竊取、送出表單等操作。
同源概述
同源政策是Netscape公司提出的一種安全政策,基本上所有支援JavaScript的浏覽器都使用該測了。隻有同源網頁中可以共享cookie,是為了保證使用者的資訊安全,防止惡意的網站過來竊取使用者資料。所謂同源政策就是規定浏覽器中的兩個URL位址的
協定、域名、端口
相同。
同源示例
網址:
http://www.test.com/dir1/test.html
。
其中,協定是
http://
,域名是
www.test.com
,端口是
8080
(預設省略)。
同源情況如下
網址 | 同源情況 |
---|---|
http://www.test.com/dir2/test.html | 同源(協定、域名和端口相同) |
https://www.test.com/dir1/test.html | 不同源(協定不同) |
http://test.com/dir1/test.html | 不同源(域名不同) |
http://www.test.com:8081/dir1/test.html | 不同源(端口不同) |
跨域
跨域概述
由于浏覽器的
同源政策
限制,要求url的
協定、域名和端口都需要相同
,而浏覽器通路中同頁面下會遇到
跨域
操作,即請求的url
協定、域名和端口中有和目前網頁url不同
的情形。
比如一個資源在與該資源本身所在伺服器的不同域、端口中請求一個資源時,資源就會發起一個跨域請求,比如
http://www.baidu.com
的某個html頁面中通過通路圖檔的
http://www.google.com/image1.jpg
,這就會發出一個跨域http請求。由于同源限制,浏覽器限制從頁面腳本内發起跨域請求,隻能加載本域下的資源。如何解決?這個時候就需要使用CORS機制。
CORS概述
跨域資源共享(CORS, Cross-Origin Resource Sharing)是一個W3C标準,允許浏覽器向跨源伺服器送出請求,它是使用額外的HTTP頭部告知浏覽器,允許web應用從不同源伺服器上通路指定資源,進而突破AJAX的同源政策限制。
CORS需要浏覽器和伺服器都支援,而浏覽器會自動完成CORS通信,重點是伺服器實作CORS接口,這樣才能保證CORS跨域通信。
CORS分類
CORS請求分為兩類:
簡單請求和非簡單請求(預先請求)
當CORS請求同時滿足以下三個條件時就使用
簡單請求
,否則即為非簡單請求。
- 請求方法是下列之一:
請求方法 |
---|
GET |
HEAD |
POST |
- 請求頭中的Content-Type請求頭的值是下列之一:
Content-Type請求頭值 |
---|
application/x-www-form-urlencoded |
multipart/form-data |
text/plain |
- Fetch規範定義CORS安全頭的集合(跨域請求中自定義的頭屬于安全頭的集合)
Fetch規範的安全頭 |
---|
Accept |
Accept-Language |
Content-Language |
Content-Type |
DPR |
Downlink |
Save-Data |
Viewport-Width |
Width |
簡單請求
簡單請求的請求示例
簡單請求即為浏覽器直接發出CORS請求,就是在頭資訊中自動添加一個
Origin
字段。如下舉例:
GET /cors HTTP/1.1
Host: www.user.com
Origin: http://www.test.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0..
其中,
Origin
說明了源(協定、域名、端口),傳送到伺服器後,由伺服器根據
Origin值
來決定是否允許這次請求。
簡單請求的響應示例
當收到簡單請求後,若伺服器允許通路
Origin指定的源
,伺服器則會多幾個頭資訊字段用來辨別(如下舉例),否則,伺服器會傳回一個正常的HTTP響應。
Access-Control-Allow-Origin: http://www.test.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: Test01
Content-Type: text/html; charset=utf-8
頭資訊介紹
-
Access-Control-Allow-Origin
必選字段,要麼是浏覽器請求時傳過來的
值,表示接受該域名請求;要麼是Origin
,表示接受任意域名請求。*
-
Access-Control-Allow-Credentials
可選字段,布爾值,代表是否允許發送cookie。預設cookie不包含在CORS請求中,是以需要設定為true,這樣伺服器允許浏覽器将cookie包含在請求中發送給伺服器(需要注意的是若要發送cookie,
不可以為Access-Control-Allow-Origin
,必須指定為浏覽器請求時傳來的Origin值。);否則,設定為false或删除該字段,伺服器不允許浏覽器發送cookie。*
-
Access-Control-Expose-Headers
可選字段,CORS請求時,XMLHttpResquest對象的getResponseHeader()方法隻可以取到6個基本字段(
),若需要取除此以外的其他字段,需通過該字段進行指定,如上述Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma
将取出Access-Control-Expose-Headers: Test01
的字段值。Test01
非簡單請求
非簡單請求即為對伺服器有特殊要求的請求,即在浏覽器頁面發出的不是簡單請求的請求,是不是有點繞?
那我們就對比一開始說的簡單請求同時需要滿足的三個條件,挑出不是那三個條件的任意,即為非簡單請求,比如請求方法是
PUT
或者
DELETE
,
Content-Type
請求頭的值為
application/json
的。
非簡單請求發出後,并不會立即執行對應的請求代碼,在雙方正式通信之前會
觸發預先請求模式
,預先請求模式會發出預先驗證的請求(
預檢請求
),執行一次正常的HTTP查詢操作,是一個
OPETIONS請求
,用于查詢要被跨域通路的伺服器是否允許目前域名下的頁面發送跨域請求,可以使用那些HTTP動詞、頭資訊字段等,當得到伺服器授權确認後,浏覽器方可發送真正的XMLHttpRequest請求。
預檢請求的請求示例
OPTIONS /cors HTTP/1.1
Host: www.user.com
Origin: http://www.test.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header,content-type
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
非簡單請求發出的預先驗證請求是
OPETIONS請求
,用于詢問伺服器是否允許本次跨域;
Origin
值表示源。
-
Access-Control-Request-Method
必選字段,該字段指列出的是浏覽器跨域請求的方法是哪些,如PUT/DELETE
-
Access-Control-Request-Headers
該字段是逗号分隔的字元串,指定浏覽器CORS請求額外發送的頭資訊字段。
預檢請求的響應示例
通過預檢請求
HTTP/1.1 200 OK
Date: Aug, 01 Dec 2020 20:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://www.test.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header,content-type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400
Content-Type: application/json; charset=utf-8
Content-Encoding: gzip
Content-Length: 100
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
- 表示伺服器允許
下的請求;若設為http://www.test.com
,表示伺服器接受任意域名請求。*
-
Access-Control-Allow-Methods
必選字段,值為逗号隔開的字元串,值為伺服器所支援的所有跨域請求方法,不限于浏覽器在預檢請求中的方法。
-
Access-Control-Allow-Headers
是否必選,需根據浏覽器請求看,若浏覽器請求中包含該字段,則預檢響應必選。值為逗号隔開的字元串,值表示伺服器所支援的所有頭資訊字段,不限于浏覽器在預檢請求中的字段。
- 可選字段,布爾值,代表是否允許發送cookie。同上述的簡單請求解釋。
-
Access-Control-Max-Age
可選字段,用于指定預檢請求的有效期,機關為秒。上述86400s表示有效期為10天,在此期間,不需要發送額外的預檢請求,會緩存該請求。
否定預檢請求
HTTP/1.1 403 Forbidden
Date: Aug, 01 Dec 2020 20:15:39 GMT
Content-Type: application/json; charset=utf-8
伺服器否定了預檢請求,會傳回一個正常的HTTP響應,浏覽器收到該響應後會觸發錯誤,被XMLHttpRequest對象的onerror回調函數捕獲,如下示例報錯資訊。
XMLHttpRequest cannot load http://www.test.com.
Origin http://www.test.com is not allowed by Access-Control-Allow-Origin.
正常請求的請求示例
伺服器通過預檢請求後,在有效期内,浏覽器都無需再發送預檢請求,都直接發送正常的CORS請求,同簡單請求一樣。
PUT /cors HTTP/1.1
Host: www.user.com
Origin: http://www.test.com
Accept-Language: en-US
X-Custom-Header: xxx
Connection: keep-alive
User-Agent: Mozilla/5.0...
其中,
Origin: http://www.test.com
是浏覽器自動添加。
正常請求的響應示例
Access-Control-Allow-Origin: http://www.test.com
Content-Type: application/json; charset=utf-8
SpringBoot解決跨域
方法一:基于@CrossOrigin配置
@CrossOrigin注解源碼
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
/** @deprecated */
@Deprecated
String[] DEFAULT_ORIGINS = new String[]{"*"};
/** @deprecated */
@Deprecated
String[] DEFAULT_ALLOWED_HEADERS = new String[]{"*"};
/** @deprecated */
@Deprecated
boolean DEFAULT_ALLOW_CREDENTIALS = true;
/** @deprecated */
@Deprecated
long DEFAULT_MAX_AGE = 1800L;
@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 -1L;
}
使用示例
@RestController
public class HiController {
@CrossOrigin(value = "http://localhost:8080")
@RequestMapping(value = "/hi", method = RequestMethod.GET)
public String callHi() {
return "hi";
}
}
在Controller層在某個方法上通過配置
@CrossOrigin注解
配置接受
http://localhost:8080
的請求,這種有局限性,且每個方法都得配置該注解。
方法二:基于CorsFilter過濾器
@Configuration
public class GlobalCorsConfig {
@Bean
public CorsFilter corsFilter() {
//new一個CorsConfiguration對象用于CORS配置資訊
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允許所有域的請求
corsConfiguration.addAllowedOrigin("*");
//允許請求攜帶認證資訊(cookies)
corsConfiguration.setAllowCredentials(true);
//允許所有的請求方法
corsConfiguration.addAllowedMethod("*");
//允許所有的請求頭
corsConfiguration.addAllowedHeader("*");
//允許暴露所有頭部資訊
corsConfiguration.addExposedHeader("*");
//添加映射路徑
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
//傳回新的CorsFilter對象
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
或寫成
@ConfigurationProperties("cors-config")
public class CorsConfig {
private CorsConfiguration buildCorsConfiguration() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.setAllowCredentials(true);
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildCorsConfiguration());
return new CorsFilter(source);
}
}
方法三:基于WebMvcConfigurerAdapter全局配置
在啟動類加:
public class Application extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedHeaders("*")
.allowedOrigins("*")
.allowedMethods("*");
}
}
或配置檔案形式
@Configuration
public class CorsConfig extends WebMvcConfigurerAdapter {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "HEAD", "POST","PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.maxAge(3600);
}
}
總結
一般SpringBoot中解決跨域用方法二和方法三,即為粗粒度,全局性配置。如果有特殊的細粒度控制到某個方法接受某域的請求,可以使用方法一。
參考
阿裡雲API網關文檔
燒不死的鳥就是鳳凰