作爲一個後端開發,我們經常遇到的一個問題就是需要配置 CORS
,好讓我們的前端能夠訪問到我們的 API,並且不讓其他人訪問。而在 Spring
中,我們見過很多種 CORS
的配置,很多資料都只是告訴我們可以這樣配置、可以那樣配置,但是這些配置有什麼區別?
CORS 是什麼
首先我們要明確,CORS
是什麼,以及規範是如何要求的。這裏只是梳理一下流程,具體的規範請看 這裏。
CORS
全稱是 Cross-Origin Resource Sharing
,直譯過來就是跨域資源共享。要理解這個概念就需要知道域、資源和同源策略這三個概念。
- 域,指的是一個站點,由
protocal
、host
和port
三部分組成,其中host
可以是域名,也可以是ip
;port
如果沒有指明,則是使用protocal
的默認端口 - 資源,是指一個
URL
對應的內容,可以是一張圖片、一種字體、一段HTML
代碼、一份JSON
數據等等任何形式的任何內容 - 同源策略,指的是爲了防止
XSS
,瀏覽器、客戶端應該僅請求與當前頁面來自同一個域的資源,請求其他域的資源需要通過驗證。
瞭解了這三個概念,我們就能理解爲什麼有 CORS
規範了:從站點 A 請求站點 B 的資源的時候,由於瀏覽器的同源策略的影響,這樣的跨域請求將被禁止發送;爲了讓跨域請求能夠正常發送,我們需要一套機制在不破壞同源策略的安全性的情況下、允許跨域請求正常發送,這樣的機制就是 CORS
。
預檢請求
在 CORS
中,定義了一種預檢請求,即 preflight request
,當實際請求不是一個 簡單請求
時,會發起一次預檢請求。預檢請求是針對實際請求的 URL 發起一次 OPTIONS
請求,並帶上下面三個 headers
:
-
Origin
:值爲當前頁面所在的域,用於告訴服務器當前請求的域。如果沒有這個header
,服務器將不會進行CORS
驗證。 -
Access-Control-Request-Method
:值爲實際請求將會使用的方法 -
Access-Control-Request-Headers
:值爲實際請求將會使用的header
集合
如果服務器端 CORS
驗證失敗,則會返回客戶端錯誤,即 4xx
的狀態碼。
否則,將會請求成功,返回 200
的狀態碼,並帶上下面這些 headers
:
-
Access-Control-Allow-Origin
:允許請求的域,多數情況下,就是預檢請求中的Origin
的值 -
Access-Control-Allow-Credentials
:一個布爾值,表示服務器是否允許使用cookies
-
Access-Control-Expose-Headers
:實際請求中可以出現在響應中的headers
集合 -
Access-Control-Max-Age
:預檢請求返回的規則可以被緩存的最長時間,超過這個時間,需要再次發起預檢請求 -
Access-Control-Allow-Methods
:實際請求中可以使用到的方法集合
瀏覽器會根據預檢請求的響應,來決定是否發起實際請求。
小結
到這裏, 我們就知道了跨域請求會經歷的故事:
- 訪問另一個域的資源
- 有可能會發起一次預檢請求(非簡單請求,或超過了
Max-Age
) - 發起實際請求
接下來,我們看看在 Spring 中,我們是如何讓 CORS
機制在我們的應用中生效的。
幾種配置的方式
Spring 提供了多種配置 CORS
的方式,有的方式針對單個 API,有的方式可以針對整個應用;有的方式在一些情況下是等效的,而在另一些情況下卻又出現不同。我們這裏例舉幾種典型的方式來看看應該如何配置。
假設我們有一個 API:
@RestController
class HelloController {
@GetMapping("hello")
fun hello(): String {
return "Hello, CORS!"
}
}
@CrossOrigin
註解
使用@CorssOrigin
註解需要引入 Spring Web
的依賴,該註解可以作用於方法或者類,可以針對這個方法或類對應的一個或多個 API 配置 CORS
規則:
@RestController
class HelloController {
@GetMapping("hello")
@CrossOrigin(origins = ["http://localhost:8080"])
fun hello(): String {
return "Hello, CORS!"
}
}
實現 WebMvcConfigurer.addCorsMappings
方法
WebMvcConfigurer
是一個接口,它同樣來自於 Spring Web
。我們可以通過實現它的 addCorsMappings
方法來針對全局 API 配置 CORS
規則:
@Configuration
@EnableWebMvc
class MvcConfig: WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/hello")
.allowedOrigins("http://localhost:8080")
}
}
注入 CorsFilter
CorsFilter
同樣來自於 Spring Web
,但是實現 WebMvcConfigurer.addCorsMappings
方法並不會使用到這個類,具體原因我們後面來分析。我們可以通過注入一個 CorsFilter
來使用它:
@Configuration
class CORSConfiguration {
@Bean
fun corsFilter(): CorsFilter {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("http://localhost:8080")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/hello", configuration)
return CorsFilter(source)
}
}
注入 CorsFilter
不止這一種方式,我們還可以通過注入一個 FilterRegistrationBean
來實現,這裏就不給例子了。
在僅僅引入Spring Web
的情況下,實現WebMvcConfigurer.addCorsMappings
方法和注入CorsFilter
這兩種方式可以達到同樣的效果,二選一即可。它們的區別會在引入Spring Security
之後會展現出來,我們後面再來分析。
Spring Security 中的配置
在引入了 Spring Security
之後,我們會發現前面的方法都不能正確的配置 CORS
,每次 preflight request
都會得到一個 401
的狀態碼,表示請求沒有被授權。這時,我們需要增加一點配置才能讓 CORS
正常工作:
@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity?) {
http?.cors()
}
}
或者,乾脆不實現 WebMvcConfigurer.addCorsMappings
方法或者注入 CorsFilter
,而是注入一個 CorsConfigurationSource
,同樣能與上面的代碼配合,正確的配置 CORS
:
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("http://localhost:8080")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/hello", configuration)
return source
}
到此,我們已經看過了幾種典型的例子了,完整的內容可以在 Demo 中查看,我們接下來看看 Spring 到底是如何實現 CORS
驗證的。
這些配置有什麼區別
我們會主要分析實現 WebMvcConfigurer.addCorsMappings
方法和調用 HttpSecurity.cors
方法這兩種方式是如何實現 CORS
的,但在進行之前,我們要先複習一下 Filter
與 Interceptor
的概念。
Filter 與 Interceptor
上圖很形象的說明了 Filter
與 Interceptor
的區別,一個作用在 DispatcherServlet
調用前,一個作用在調用後。
但實際上,它們本身並沒有任何關係,是完全獨立的概念。
Filter
由 Servlet
標準定義,要求 Filter
需要在 Servlet
被調用之前調用,作用顧名思義,就是用來過濾請求。在 Spring Web
應用中,DispatcherServlet
就是唯一的 Servlet
實現。
Interceptor
由 Spring 自己定義,由 DispatcherServlet
調用,可以定義在 Handler
調用前後的行爲。這裏的 Handler
,在多數情況下,就是我們的 Controller
中對應的方法。
對於 Filter
和 Interceptor
的複習就到這裏,我們只需要知道它們會在什麼時候被調用到,就能理解後面的內容了。
WebMvcConfigurer.addCorsMappings
方法做了什麼
我們從 WebMvcConfigurer.addCorsMappings
方法的參數開始,先看看 CORS
配置是如何保存到 Spring 上下文中的,然後在瞭解一下 Spring 是如何使用的它們。
注入 CORS 配置
CorsRegistry 和 CorsRegistration
WebMvcConfigurer.addCorsMappings
方法的參數 CorsRegistry
用於註冊 CORS
配置,它的源碼如下:
public class CorsRegistry {
private final List<CorsRegistration> registrations = new ArrayList<>();
public CorsRegistration addMapping(String pathPattern) {
CorsRegistration registration = new CorsRegistration(pathPattern);
this.registrations.add(registration);
return registration;
}
protected Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());
for (CorsRegistration registration : this.registrations) {
configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
}
return configs;
}
}
我們發現這個類僅僅有兩個方法:
-
addMapping
接收一個pathPattern
,創建一個CorsRegistration
實例,保存到列表後將其返回。在我們的代碼中,這裏的pathPattern
就是/hello
-
getCorsConfigurations
方法將保存的CORS
規則轉換成Map
後返回
CorsRegistration
這個類,同樣很簡單,我們看看它的部分源碼:
public class CorsRegistration {
private final String pathPattern;
private final CorsConfiguration config;
public CorsRegistration(String pathPattern) {
this.pathPattern = pathPattern;
this.config = new CorsConfiguration().applyPermitDefaultValues();
}
public CorsRegistration allowedOrigins(String... origins) {
this.config.setAllowedOrigins(Arrays.asList(origins));
return this;
}
}
不難發現,這個類僅僅保存了一個 pathPattern
字符串和 CorsConfiguration
,很好理解,它保存的是一個 pathPattern
對應的 CORS
規則。
在它的構造函數中,調用的 CorsConfiguration.applyPermitDefaultValues
方法則用於配置默認的 CORS
規則:
- allowedOrigins 默認爲所有域
- allowedMethods 默認爲
GET
、HEAD
和POST
- allowedHeaders 默認爲所有
- maxAge 默認爲 30 分鐘
- exposedHeaders 默認爲 null,也就是不暴露任何 header
- credentials 默認爲 null
創建 CorsRegistration
後,我們可以通過它的 allowedOrigins
、allowedMethods
等方法修改它的 CorsConfiguration
,覆蓋掉上面的默認值。
現在,我們已經通過 WebMvcConfigurer.addCorsMappings
方法配置好 CorsRegistry
了,接下來看看這些配置會在什麼地方被注入到 Spring 上下文中。
WebMvcConfigurationSupport
CorsRegistry.getCorsConfigurations
方法,會被 WebMvcConfigurationSupport.getConfigurations
方法調用,這個方法如下:
protected final Map<String, CorsConfiguration> getCorsConfigurations() {
if (this.corsConfigurations == null) {
CorsRegistry registry = new CorsRegistry();
addCorsMappings(registry);
this.corsConfigurations = registry.getCorsConfigurations();
}
return this.corsConfigurations;
}
addCorsMappings(registry)
調用的是自己的方法,由子類DelegatingWebMvcConfiguration
通過委託的方式調用到WebMvcConfigurer.addCorsMappings
方法,我們的配置也由此被讀取到。
getCorsConfigurations
是一個 protected
方法,是爲了在擴展該類時,仍然能夠直接獲取到 CORS
配置。而這個方法在這個類裏被四個地方調用到,這四個調用的地方,都是爲了註冊一個 HandlerMapping
到 Spring 容器中。每一個地方都會調用 mapping.setCorsConfigurations
方法來接收 CORS
配置,而這個 setCorsConfigurations
方法,則由 AbstractHandlerMapping
提供,CorsConfigurations
也被保存在這個抽象類中。
到此,我們的 CORS
配置藉由 AbstractHandlerMapping
被注入到了多個 HandlerMapping
中,而這些 HandlerMapping
以 Spring 組件的形式被註冊到了 Spring 容器中,當請求來臨時,將會被調用。
獲取 CORS 配置
還記得前面關於 Filter
和 Interceptor
那張圖嗎?當請求來到 Spring Web
時,一定會到達 DispatcherServlet
這個唯一的 Servlet
。
在 DispatcherServlet.doDispatch
方法中,會調用所有 HandlerMapping.getHandler
方法。好巧不巧,這個方法又是由 AbstractHandlerMapping
實現的:
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 省略代碼
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
在這個方法中,關於 CORS
的部分都在這個 if
中。我們來看看最後這個 getCorsHandlerExecutionChain
做了什麼:
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
else {
chain.addInterceptor(new CorsInterceptor(config));
}
return chain;
}
可以看到:
- 針對
preflight request
,由於不會有對應的Handler
來處理,所以這裏就創建了一個PreFlightHandler
來作爲這次請求的handler
- 對於其他的跨域請求,因爲會有對應的
handler
,所以就在handlerExecutionChain
中加入一個CorsInterceptor
來進行CORS
驗證
這裏的 PreFlightHandler
和 CorsInterceptor
都是 AbstractHandlerMapping
的內部類,實現幾乎一致,區別僅僅在於一個是 HttpRequestHandler
,一個是 HandlerInterceptor
;它們對 CORS
規則的驗證都交由 CorsProcessor
接口完成,這裏採用了默認實現 DefaultCorsProcessor
。
DefaultCorsProcessor
則是依照 CORS
標準來實現,並在驗證失敗的時候打印 debug
日誌並拒絕請求。我們只需要關注一下標準中沒有定義的驗證失敗時的狀態碼:
protected void rejectRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
}
CORS
驗證失敗時調用這個方法,並設置狀態碼爲 403
。
小結
通過對源碼的研究,我們發現實現 WebMvcConfigurer.addCorsMappings
方法的方式配置 CORS
,會在 Interceptor
或者 Handler
層進行 CORS
驗證。
HtttpSecurity.cors
方法做了什麼
在研究這個方法的行爲之前,我們先來回想一下,我們調用這個方法解決的是什麼問題。
前面我們通過某種方式配置好 CORS
後,引入 Spring Security
,CORS
就失效了,直到調用這個方法後,CORS
規則才重新生效。
下面這些原因,導致了 preflight request
無法通過身份驗證,從而導致 CORS
失效:
-
preflight request
不會攜帶認證信息 -
Spring Security
通過Filter
來進行身份驗證 -
Interceptor
和HttpRequestHanlder
在DispatcherServlet
之後被調用 -
Spring Security
中的Filter
優先級比我們注入的CorsFilter
優先級高
接下來我們就來看看 HttpSecurity.cors
方法是如何解決這個問題的。
CorsConfigurer 如何配置 CORS 規則
HttpSecurity.cors
方法中其實只有一行代碼:
public CorsConfigurer<HttpSecurity> cors() throws Exception {
return getOrApply(new CorsConfigurer<>());
}
這裏調用的 getOrApply
方法會將 SecurityConfigurerAdapter
的子類實例加入到它的父類 AbstractConfiguredSecurityBuilder
維護的一個 Map
中,然後一個個的調用 configure
方法。所以,我們來關注一下 CorsConfigurer.configure
方法就好了。
@Override
public void configure(H http) throws Exception {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
CorsFilter corsFilter = getCorsFilter(context);
if (corsFilter == null) {
throw new IllegalStateException(
"Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a "
+ CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean.");
}
http.addFilter(corsFilter);
}
這段代碼很好理解,就是在當前的 Spring Context 中找到一個 CorsFilter
,然後將它加入到 http
對象的 filters
中。由上面的 HttpSecurity.cors
方法可知,這裏的 http
對象實際類型就是 HttpSecurity
。
getCorsFilter 方法做了什麼
也許你會好奇,HttpSecurity
要如何保證 CorsFilter
一定在 Spring Security
的 Filters
之前調用。但是在研究這個之前,我們先來看看同樣重要的 getCorsFilter
方法,這裏可以解答我們前面的一些疑問。
private CorsFilter getCorsFilter(ApplicationContext context) {
if (this.configurationSource != null) {
return new CorsFilter(this.configurationSource);
}
boolean containsCorsFilter = context
.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
if (containsCorsFilter) {
return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
}
boolean containsCorsSource = context
.containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
if (containsCorsSource) {
CorsConfigurationSource configurationSource = context.getBean(
CORS_CONFIGURATION_SOURCE_BEAN_NAME, CorsConfigurationSource.class);
return new CorsFilter(configurationSource);
}
boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR,
context.getClassLoader());
if (mvcPresent) {
return MvcCorsFilter.getMvcCorsFilter(context);
}
return null;
}
這是 CorsConfigurer
尋找 CorsFilter
的全部邏輯,我們用人話來說就是:
-
CorsConfigurer
自己是否有配置CorsConfigurationSource
,如果有的話,就用它創建一個CorsFilter
。 - 在當前的上下文中,是否存在一個名爲
corsFilter
的實例,如果有的話,就把他當作一個CorsFilter
來用。 - 在當前的上下文中,是否存在一個名爲
corsConfigurationSource
的CorsConfigurationSource
實例,如果有的話,就用它創建一個CorsFilter
。 - 在當前上下文的類加載器中,是否存在類
HandlerMappingIntrospector
,如果有的話,則通過MvcCorsFilter
這個內部類創建一個CorsFilter
。 - 如果沒有找到,那就返回一個
null
,調用的地方最後會拋出異常,阻止 Spring 初始化。
上面的第 2、3、4 步能解答我們前面的配置爲什麼生效,以及它們的區別。
註冊 CorsFilter
的方式,這個 Filter
最終會被直接註冊到 Servlet container 中被使用到。
註冊 CorsConfigurationSource
的方式,會用這個 source
創建一個 CorsFiltet
然後註冊到 Servlet container 中被使用到。
而第四步的情況比較複雜。HandlerMappingIntrospector
是 Spring Web
提供的一個類,實現了 CorsConfigurationSource
接口,所以在 MvcCorsFilter
中,它被直接用於創建 CorsFilter
。它實現的 getCorsConfiguration
方法,會經歷:
- 遍歷
HandlerMapping
- 調用
getHandler
方法得到HandlerExecutionChain
- 從中找到
CorsConfigurationSource
的實例 - 調用這個實例的
getCorsConfiguration
方法,返回得到的CorsConfiguration
所以得到的 CorsConfigurationSource
實例,實際上就是前面講到的 CorsInterceptor
或者 PreFlightHandler
。
所以第四步實際上匹配的是實現 WebMvcConfigurer.addCorsMappings
方法的方式。
由於在 CorsFilter
中每次處理請求時都會調用 CorsConfigurationSource.getCorsConfiguration
方法,而 DispatcherServlet
中也會每次調用 HandlerMapping.getHandler
方法,再加上這時的 HandlerExecutionChain
中還有 CorsInterceptor
,所以使用這個方式相對於其他方式,做了很多重複的工作。所以 WebMvcConfigurer.addCorsMappings
+ HttpSecurity.cors
的方式降低了我們代碼的效率,也許微乎其微,但能避免的情況下,還是不要使用。
HttpSecurity 中的 filters 屬性
在 CorsConfigurer.configure
方法中調用的 HttpSecurity.addFilter
方法,由它的父類 HttpSecurityBuilder
聲明,並約定了很多 Filter
的順序。然而 CorsFilter
並不在其中。不過在 Spring Security
中,目前還只有 HttpSecurity
這一個實現,所以我們來看看這裏的代碼實現就知道 CorsFilter
會排在什麼地方了。
public HttpSecurity addFilter(Filter filter) {
Class<? extends Filter> filterClass = filter.getClass();
if (!comparator.isRegistered(filterClass)) {
throw new IllegalArgumentException("...");
}
this.filters.add(filter);
return this;
}
我們可以看到,Filter
會被直接加到 List
中,而不是按照一定的順序來加入的。但同時,我們也發現了一個 comparator
對象,並且只有被註冊到了該類的 Filter
才能被加入到 filters
屬性中。這個 comparator
又是用來做什麼的呢?
在 Spring Security 創建過程中,會調用到 HttpSeciryt.performBuild
方法,在這裏我們可以看到 filters
和 comparator
是如何被使用到的。
protected DefaultSecurityFilterChain performBuild() throws Exception {
Collections.sort(filters, comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
可以看到,Spring Security 使用了這個 comparator
在獲取 SecurityFilterChain
的時候來保證 filters
的順序,所以,研究這個 comparator
就能知道在 SecurityFilterChain
中的那些 Filter
的順序是如何的了。
這個 comparator
的類型是 FilterComparator
,從名字就能看出來是專用於 Filter
比較的類,它的實現也並不神祕,從構造函數就能猜到是如何實現的:
FilterComparator() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(ChannelProcessingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
// 省略代碼
}
可以看到 CorsFilter
排在了第六位,在所有的 Security Filter 之前,由此便解決了 preflight request
沒有攜帶認證信息的問題。
小結
引入 Spring Security
之後,我們的 CORS
驗證實際上是依然運行着的,只是因爲 preflight request
不會攜帶認證信息,所以無法通過身份驗證。使用 HttpSecurity.cors
方法會幫助我們在當前的 Spring Context 中找到或創建一個 CorsFilter
並安排在身份驗證的 Filter
之前,以保證能對 preflight request
正確處理。
總結
研究了 Spring 中 CORS 的代碼,我們瞭解到了這樣一些知識:
- 實現
WebMvcConfigurer.addCorsMappings
方法來進行的CORS
配置,最後會在 Spring 的Interceptor
或Handler
中生效 - 注入
CorsFilter
的方式會讓CORS
驗證在Filter
中生效 - 引入
Spring Security
後,需要調用HttpSecurity.cors
方法以保證CorsFilter
會在身份驗證相關的Filter
之前執行 -
HttpSecurity.cors
+WebMvcConfigurer.addCorsMappings
是一種相對低效的方式,會導致跨域請求分別在Filter
和Interceptor
層各經歷一次CORS
驗證 -
HttpSecurity.cors
+ 註冊CorsFilter
與HttpSecurity.cors
+ 註冊CorsConfigurationSource
在運行的時候是等效的 - 在 Spring 中,沒有通過
CORS
驗證的請求會得到狀態碼爲 403 的響應