關注微信公眾號: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
頭信息介紹
-
Access-Control-Allow-Origin
表示服務器允許http://www.test.com
下的請求;若設為*
,表示服務器接受任意域名請求。 -
Access-Control-Allow-Methods
必選字段,值為逗號隔開的字符串,值為服務器所支持的所有跨域請求方法,不限於瀏覽器在預檢請求中的方法。 -
Access-Control-Allow-Headers
是否必選,需根據瀏覽器請求看,若瀏覽器請求中包含該字段,則預檢響應必選。值為逗號隔開的字符串,值表示服務器所支持的所有頭信息字段,不限於瀏覽器在預檢請求中的字段。 -
Access-Control-Allow-Credentials
可選字段,布爾值,代表是否允許發送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中解決跨域用方法二和方法三,即為粗粒度,全局性配置。如果有特殊的細粒度控制到某個方法接受某域的請求,可以使用方法一。