溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Spring 里那么多種 CORS 的配置方式,到底有什么區(qū)別

發(fā)布時(shí)間:2020-07-21 08:02:09 來源:網(wǎng)絡(luò) 閱讀:1412 作者:wx5d30212829a35 欄目:編程語言

作為一個(gè)后端開發(fā),我們經(jīng)常遇到的一個(gè)問題就是需要配置 CORS,好讓我們的前端能夠訪問到我們的 API,并且不讓其他人訪問。而在 Spring 中,我們見過很多種 CORS 的配置,很多資料都只是告訴我們可以這樣配置、可以那樣配置,但是這些配置有什么區(qū)別?

CORS 是什么

首先我們要明確,CORS 是什么,以及規(guī)范是如何要求的。這里只是梳理一下流程,具體的規(guī)范請(qǐng)看 這里。

CORS 全稱是 Cross-Origin Resource Sharing,直譯過來就是跨域資源共享。要理解這個(gè)概念就需要知道資源同源策略這三個(gè)概念。

  • 域,指的是一個(gè)站點(diǎn),由 protocal、host 和 port 三部分組成,其中 host 可以是域名,也可以是 ip ;port如果沒有指明,則是使用 protocal 的默認(rèn)端口

  • 資源,是指一個(gè) URL 對(duì)應(yīng)的內(nèi)容,可以是一張圖片、一種字體、一段 HTML 代碼、一份 JSON 數(shù)據(jù)等等任何形式的任何內(nèi)容

  • 同源策略,指的是為了防止 XSS,瀏覽器、客戶端應(yīng)該僅請(qǐng)求與當(dāng)前頁面來自同一個(gè)域的資源,請(qǐng)求其他域的資源需要通過驗(yàn)證。

了解了這三個(gè)概念,我們就能理解為什么有 CORS 規(guī)范了:從站點(diǎn) A 請(qǐng)求站點(diǎn) B 的資源的時(shí)候,由于瀏覽器的同源策略的影響,這樣的跨域請(qǐng)求將被禁止發(fā)送;為了讓跨域請(qǐng)求能夠正常發(fā)送,我們需要一套機(jī)制在不破壞同源策略的安全性的情況下、允許跨域請(qǐng)求正常發(fā)送,這樣的機(jī)制就是 CORS。

預(yù)檢請(qǐng)求

在 CORS 中,定義了一種預(yù)檢請(qǐng)求,即 preflight request,當(dāng)實(shí)際請(qǐng)求不是一個(gè) 簡單請(qǐng)求 時(shí),會(huì)發(fā)起一次預(yù)檢請(qǐng)求。預(yù)檢請(qǐng)求是針對(duì)實(shí)際請(qǐng)求的 URL 發(fā)起一次 OPTIONS 請(qǐng)求,并帶上下面三個(gè) headers :

  • Origin:值為當(dāng)前頁面所在的域,用于告訴服務(wù)器當(dāng)前請(qǐng)求的域。如果沒有這個(gè) header,服務(wù)器將不會(huì)進(jìn)行 CORS驗(yàn)證。

  • Access-Control-Request-Method:值為實(shí)際請(qǐng)求將會(huì)使用的方法

  • Access-Control-Request-Headers:值為實(shí)際請(qǐng)求將會(huì)使用的 header 集合

如果服務(wù)器端 CORS 驗(yàn)證失敗,則會(huì)返回客戶端錯(cuò)誤,即 4xx 的狀態(tài)碼。

否則,將會(huì)請(qǐng)求成功,返回 200 的狀態(tài)碼,并帶上下面這些 headers:

  • Access-Control-Allow-Origin:允許請(qǐng)求的域,多數(shù)情況下,就是預(yù)檢請(qǐng)求中的 Origin 的值

  • Access-Control-Allow-Credentials:一個(gè)布爾值,表示服務(wù)器是否允許使用 cookies

  • Access-Control-Expose-Headers:實(shí)際請(qǐng)求中可以出現(xiàn)在響應(yīng)中的 headers 集合

  • Access-Control-Max-Age:預(yù)檢請(qǐng)求返回的規(guī)則可以被緩存的最長時(shí)間,超過這個(gè)時(shí)間,需要再次發(fā)起預(yù)檢請(qǐng)求

  • Access-Control-Allow-Methods:實(shí)際請(qǐng)求中可以使用到的方法集合

瀏覽器會(huì)根據(jù)預(yù)檢請(qǐng)求的響應(yīng),來決定是否發(fā)起實(shí)際請(qǐng)求。

小結(jié)

到這里, 我們就知道了跨域請(qǐng)求會(huì)經(jīng)歷的故事:

  1. 訪問另一個(gè)域的資源

  2. 有可能會(huì)發(fā)起一次預(yù)檢請(qǐng)求(非簡單請(qǐng)求,或超過了 Max-Age)

  3. 發(fā)起實(shí)際請(qǐng)求

接下來,我們看看在 Spring 中,我們是如何讓 CORS 機(jī)制在我們的應(yīng)用中生效的。

幾種配置的方式

Spring 提供了多種配置 CORS 的方式,有的方式針對(duì)單個(gè) API,有的方式可以針對(duì)整個(gè)應(yīng)用;有的方式在一些情況下是等效的,而在另一些情況下卻又出現(xiàn)不同。我們這里例舉幾種典型的方式來看看應(yīng)該如何配置。

假設(shè)我們有一個(gè) API:

@RestController
class?HelloController?{
?@GetMapping("hello")
?fun?hello():?String?{
?return?"Hello,?CORS!"
?}
}

@CrossOrigin 注解

使用@CorssOrigin 注解需要引入 Spring Web 的依賴,該注解可以作用于方法或者類,可以針對(duì)這個(gè)方法或類對(duì)應(yīng)的一個(gè)或多個(gè) API 配置 CORS 規(guī)則:

@RestController
class?HelloController?{
?@GetMapping("hello")
?@CrossOrigin(origins?=?["http://localhost:8080"])
?fun?hello():?String?{
?return?"Hello,?CORS!"
?}
}

實(shí)現(xiàn) WebMvcConfigurer.addCorsMappings 方法

WebMvcConfigurer 是一個(gè)接口,它同樣來自于 Spring Web。我們可以通過實(shí)現(xiàn)它的 addCorsMappings 方法來針對(duì)全局 API 配置 CORS 規(guī)則:

@Configuration
@EnableWebMvc
class?MvcConfig:?WebMvcConfigurer?{
?override?fun?addCorsMappings(registry:?CorsRegistry)?{
?registry.addMapping("/hello")
?.allowedOrigins("http://localhost:8080")
?}
}

注入 CorsFilter

CorsFilter 同樣來自于 Spring Web,但是實(shí)現(xiàn) WebMvcConfigurer.addCorsMappings 方法并不會(huì)使用到這個(gè)類,具體原因我們后面來分析。我們可以通過注入一個(gè) 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 不止這一種方式,我們還可以通過注入一個(gè) FilterRegistrationBean 來實(shí)現(xiàn),這里就不給例子了。

在僅僅引入 Spring Web 的情況下,實(shí)現(xiàn) WebMvcConfigurer.addCorsMappings 方法和注入 CorsFilter 這兩種方式可以達(dá)到同樣的效果,二選一即可。它們的區(qū)別會(huì)在引入 Spring Security 之后會(huì)展現(xiàn)出來,我們后面再來分析。

Spring Security 中的配置

在引入了 Spring Security 之后,我們會(huì)發(fā)現(xiàn)前面的方法都不能正確的配置 CORS,每次 preflight request 都會(huì)得到一個(gè) 401 的狀態(tài)碼,表示請(qǐng)求沒有被授權(quán)。這時(shí),我們需要增加一點(diǎn)配置才能讓 CORS 正常工作:

@Configuration
class?SecurityConfig?:?WebSecurityConfigurerAdapter()?{
?override?fun?configure(http:?HttpSecurity?)?{
?http?.cors()
?}
}

或者,干脆不實(shí)現(xiàn) WebMvcConfigurer.addCorsMappings 方法或者注入 CorsFilter ,而是注入一個(gè) CorsConfigurationSource ,同樣能與上面的代碼配合,正確的配置 CORS:

@Bean
fun?corsConfigurationSource():?CorsConfigurationSource?{
?val?configuration?=?CorsConfiguration()
?configuration.allowedOrigins?=?listOf("http://localhost:8080")
?val?source?=?UrlBasedCorsConfigurationSource()
?source.registerCorsConfiguration("/hello",?configuration)
?return?source
}

到此,我們已經(jīng)看過了幾種典型的例子了,完整的內(nèi)容可以在 Demo 中查看,我們接下來看看 Spring 到底是如何實(shí)現(xiàn) CORS 驗(yàn)證的。

這些配置有什么區(qū)別

我們會(huì)主要分析實(shí)現(xiàn) WebMvcConfigurer.addCorsMappings 方法和調(diào)用 HttpSecurity.cors 方法這兩種方式是如何實(shí)現(xiàn) CORS 的,但在進(jìn)行之前,我們要先復(fù)習(xí)一下 Filter 與 Interceptor 的概念。

Filter 與 Interceptor

上圖很形象的說明了 Filter 與 Interceptor 的區(qū)別,一個(gè)作用在 DispatcherServlet 調(diào)用前,一個(gè)作用在調(diào)用后。

但實(shí)際上,它們本身并沒有任何關(guān)系,是完全獨(dú)立的概念。

Filter 由 Servlet 標(biāo)準(zhǔn)定義,要求 Filter 需要在 Servlet 被調(diào)用之前調(diào)用,作用顧名思義,就是用來過濾請(qǐng)求。在 Spring Web 應(yīng)用中,DispatcherServlet 就是唯一的 Servlet 實(shí)現(xiàn)。

Interceptor 由 Spring 自己定義,由 DispatcherServlet 調(diào)用,可以定義在 Handler 調(diào)用前后的行為。這里的 Handler ,在多數(shù)情況下,就是我們的 Controller 中對(duì)應(yīng)的方法。

對(duì)于 Filter 和 Interceptor 的復(fù)習(xí)就到這里,我們只需要知道它們會(huì)在什么時(shí)候被調(diào)用到,就能理解后面的內(nèi)容了。

WebMvcConfigurer.addCorsMappings 方法做了什么

我們從 WebMvcConfigurer.addCorsMappings 方法的參數(shù)開始,先看看 CORS 配置是如何保存到 Spring 上下文中的,然后在了解一下 Spring 是如何使用的它們。

注入 CORS 配置

CorsRegistry 和 CorsRegistration

WebMvcConfigurer.addCorsMappings 方法的參數(shù) CorsRegistry 用于注冊(cè) 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;
?}
}

我們發(fā)現(xiàn)這個(gè)類僅僅有兩個(gè)方法:

  • addMapping 接收一個(gè) pathPattern,創(chuàng)建一個(gè) CorsRegistration 實(shí)例,保存到列表后將其返回。在我們的代碼中,這里的 pathPattern 就是 /hello

  • getCorsConfigurations 方法將保存的 CORS 規(guī)則轉(zhuǎn)換成 Map 后返回

CorsRegistration 這個(gè)類,同樣很簡單,我們看看它的部分源碼:

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;
?}
}

不難發(fā)現(xiàn),這個(gè)類僅僅保存了一個(gè) pathPattern 字符串和 CorsConfiguration,很好理解,它保存的是一個(gè) pathPattern 對(duì)應(yīng)的 CORS 規(guī)則。

在它的構(gòu)造函數(shù)中,調(diào)用的 CorsConfiguration.applyPermitDefaultValues 方法則用于配置默認(rèn)的 CORS 規(guī)則:

  • allowedOrigins 默認(rèn)為所有域

  • allowedMethods 默認(rèn)為 GET 、HEAD 和 POST

  • allowedHeaders 默認(rèn)為所有

  • maxAge 默認(rèn)為 30 分鐘

  • exposedHeaders 默認(rèn)為 null,也就是不暴露任何 header

  • credentials 默認(rèn)為 null

創(chuàng)建 CorsRegistration 后,我們可以通過它的 allowedOrigins、allowedMethods 等方法修改它的 CorsConfiguration,覆蓋掉上面的默認(rèn)值。

現(xiàn)在,我們已經(jīng)通過 WebMvcConfigurer.addCorsMappings 方法配置好 CorsRegistry 了,接下來看看這些配置會(huì)在什么地方被注入到 Spring 上下文中。

WebMvcConfigurationSupport

CorsRegistry.getCorsConfigurations 方法,會(huì)被 WebMvcConfigurationSupport.getConfigurations 方法調(diào)用,這個(gè)方法如下:

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) 調(diào)用的是自己的方法,由子類 DelegatingWebMvcConfiguration 通過委托的方式調(diào)用到 WebMvcConfigurer.addCorsMappings 方法,我們的配置也由此被讀取到。

getCorsConfigurations 是一個(gè) protected 方法,是為了在擴(kuò)展該類時(shí),仍然能夠直接獲取到 CORS 配置。而這個(gè)方法在這個(gè)類里被四個(gè)地方調(diào)用到,這四個(gè)調(diào)用的地方,都是為了注冊(cè)一個(gè) HandlerMapping 到 Spring 容器中。每一個(gè)地方都會(huì)調(diào)用 mapping.setCorsConfigurations 方法來接收 CORS 配置,而這個(gè) setCorsConfigurations 方法,則由 AbstractHandlerMapping 提供,CorsConfigurations 也被保存在這個(gè)抽象類中。

到此,我們的 CORS 配置借由 AbstractHandlerMapping 被注入到了多個(gè) HandlerMapping 中,而這些 HandlerMapping以 Spring 組件的形式被注冊(cè)到了 Spring 容器中,當(dāng)請(qǐng)求來臨時(shí),將會(huì)被調(diào)用。

獲取 CORS 配置

還記得前面關(guān)于 Filter 和 Interceptor 那張圖嗎?當(dāng)請(qǐng)求來到 Spring Web 時(shí),一定會(huì)到達(dá) DispatcherServlet 這個(gè)唯一的 Servlet。

在 DispatcherServlet.doDispatch 方法中,會(huì)調(diào)用所有 HandlerMapping.getHandler 方法。好巧不巧,這個(gè)方法又是由 AbstractHandlerMapping 實(shí)現(xiàn)的:

@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;
}

在這個(gè)方法中,關(guān)于 CORS 的部分都在這個(gè) if 中。我們來看看最后這個(gè) 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;
}

可以看到:

  • 針對(duì) preflight request,由于不會(huì)有對(duì)應(yīng)的 Handler 來處理,所以這里就創(chuàng)建了一個(gè) PreFlightHandler 來作為這次請(qǐng)求的 handler

  • 對(duì)于其他的跨域請(qǐng)求,因?yàn)闀?huì)有對(duì)應(yīng)的 handler,所以就在 handlerExecutionChain 中加入一個(gè) CorsInterceptor 來進(jìn)行 CORS 驗(yàn)證

這里的 PreFlightHandler 和 CorsInterceptor 都是 AbstractHandlerMapping 的內(nèi)部類,實(shí)現(xiàn)幾乎一致,區(qū)別僅僅在于一個(gè)是 HttpRequestHandler,一個(gè)是 HandlerInterceptor;它們對(duì) CORS 規(guī)則的驗(yàn)證都交由 CorsProcessor 接口完成,這里采用了默認(rèn)實(shí)現(xiàn) DefaultCorsProcessor 。

DefaultCorsProcessor 則是依照 CORS 標(biāo)準(zhǔn)來實(shí)現(xiàn),并在驗(yàn)證失敗的時(shí)候打印 debug 日志并拒絕請(qǐng)求。我們只需要關(guān)注一下標(biāo)準(zhǔn)中沒有定義的驗(yàn)證失敗時(shí)的狀態(tài)碼:

protected?void?rejectRequest(ServerHttpResponse?response)?throws?IOException?{
?response.setStatusCode(HttpStatus.FORBIDDEN);
?response.getBody().write("Invalid?CORS?request".getBytes(StandardCharsets.UTF_8));
}

CORS 驗(yàn)證失敗時(shí)調(diào)用這個(gè)方法,并設(shè)置狀態(tài)碼為 403。


小結(jié)

通過對(duì)源碼的研究,我們發(fā)現(xiàn)實(shí)現(xiàn) WebMvcConfigurer.addCorsMappings 方法的方式配置 CORS,會(huì)在 Interceptor 或者 Handler 層進(jìn)行 CORS 驗(yàn)證。

HtttpSecurity.cors 方法做了什么

在研究這個(gè)方法的行為之前,我們先來回想一下,我們調(diào)用這個(gè)方法解決的是什么問題。

前面我們通過某種方式配置好 CORS 后,引入 Spring Security,CORS 就失效了,直到調(diào)用這個(gè)方法后,CORS 規(guī)則才重新生效。

下面這些原因,導(dǎo)致了 preflight request 無法通過身份驗(yàn)證,從而導(dǎo)致 CORS 失效:

  1. preflight request 不會(huì)攜帶認(rèn)證信息

  2. Spring Security 通過 Filter 來進(jìn)行身份驗(yàn)證

  3. Interceptor 和 HttpRequestHanlder 在 DispatcherServlet 之后被調(diào)用

  4. Spring Security 中的 Filter 優(yōu)先級(jí)比我們注入的 CorsFilter 優(yōu)先級(jí)高

接下來我們就來看看 HttpSecurity.cors 方法是如何解決這個(gè)問題的。

CorsConfigurer 如何配置 CORS 規(guī)則

HttpSecurity.cors 方法中其實(shí)只有一行代碼:

public?CorsConfigurer<HttpSecurity>?cors()?throws?Exception?{
?return?getOrApply(new?CorsConfigurer<>());
}

這里調(diào)用的 getOrApply 方法會(huì)將 SecurityConfigurerAdapter 的子類實(shí)例加入到它的父類 AbstractConfiguredSecurityBuilder 維護(hù)的一個(gè) Map 中,然后一個(gè)個(gè)的調(diào)用 configure 方法。所以,我們來關(guān)注一下 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);
}

這段代碼很好理解,就是在當(dāng)前的 Spring Context 中找到一個(gè) CorsFilter,然后將它加入到 http 對(duì)象的 filters 中。由上面的 HttpSecurity.cors 方法可知,這里的 http 對(duì)象實(shí)際類型就是 HttpSecurity。

getCorsFilter 方法做了什么

也許你會(huì)好奇,HttpSecurity 要如何保證 CorsFilter 一定在 Spring Security 的 Filters 之前調(diào)用。但是在研究這個(gè)之前,我們先來看看同樣重要的 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 的全部邏輯,我們用人話來說就是:

  1. CorsConfigurer 自己是否有配置 CorsConfigurationSource,如果有的話,就用它創(chuàng)建一個(gè) CorsFilter。

  2. 在當(dāng)前的上下文中,是否存在一個(gè)名為 corsFilter 的實(shí)例,如果有的話,就把他當(dāng)作一個(gè) CorsFilter 來用。

  3. 在當(dāng)前的上下文中,是否存在一個(gè)名為 corsConfigurationSource 的 CorsConfigurationSource 實(shí)例,如果有的話,就用它創(chuàng)建一個(gè) CorsFilter。

  4. 在當(dāng)前上下文的類加載器中,是否存在類 HandlerMappingIntrospector,如果有的話,則通過 MvcCorsFilter 這個(gè)內(nèi)部類創(chuàng)建一個(gè) CorsFilter。

  5. 如果沒有找到,那就返回一個(gè) null,調(diào)用的地方最后會(huì)拋出異常,阻止 Spring 初始化。

上面的第 2、3、4 步能解答我們前面的配置為什么生效,以及它們的區(qū)別。

注冊(cè) CorsFilter 的方式,這個(gè) Filter 最終會(huì)被直接注冊(cè)到 Servlet container 中被使用到。

注冊(cè) CorsConfigurationSource 的方式,會(huì)用這個(gè) source 創(chuàng)建一個(gè) CorsFiltet 然后注冊(cè)到 Servlet container 中被使用到。

而第四步的情況比較復(fù)雜。HandlerMappingIntrospector 是 Spring Web 提供的一個(gè)類,實(shí)現(xiàn)了 CorsConfigurationSource 接口,所以在 MvcCorsFilter 中,它被直接用于創(chuàng)建 CorsFilter。它實(shí)現(xiàn)的 getCorsConfiguration 方法,會(huì)經(jīng)歷:

  1. 遍歷 HandlerMapping

  2. 調(diào)用 getHandler 方法得到 HandlerExecutionChain

  3. 從中找到 CorsConfigurationSource 的實(shí)例

  4. 調(diào)用這個(gè)實(shí)例的 getCorsConfiguration 方法,返回得到的 CorsConfiguration

所以得到的 CorsConfigurationSource 實(shí)例,實(shí)際上就是前面講到的 CorsInterceptor 或者 PreFlightHandler。

所以第四步實(shí)際上匹配的是實(shí)現(xiàn) WebMvcConfigurer.addCorsMappings 方法的方式。

由于在 CorsFilter 中每次處理請(qǐng)求時(shí)都會(huì)調(diào)用 CorsConfigurationSource.getCorsConfiguration 方法,而 DispatcherServlet 中也會(huì)每次調(diào)用 HandlerMapping.getHandler 方法,再加上這時(shí)的 HandlerExecutionChain 中還有 CorsInterceptor,所以使用這個(gè)方式相對(duì)于其他方式,做了很多重復(fù)的工作。所以 WebMvcConfigurer.addCorsMappings + HttpSecurity.cors 的方式降低了我們代碼的效率,也許微乎其微,但能避免的情況下,還是不要使用。

HttpSecurity 中的 filters 屬性

在 CorsConfigurer.configure 方法中調(diào)用的 HttpSecurity.addFilter 方法,由它的父類 HttpSecurityBuilder 聲明,并約定了很多 Filter 的順序。然而 CorsFilter 并不在其中。不過在 Spring Security 中,目前還只有 HttpSecurity 這一個(gè)實(shí)現(xiàn),所以我們來看看這里的代碼實(shí)現(xiàn)就知道 CorsFilter 會(huì)排在什么地方了。

public?HttpSecurity?addFilter(Filter?filter)?{
?Class<??extends?Filter>?filterClass?=?filter.getClass();
?if?(!comparator.isRegistered(filterClass))?{
?throw?new?IllegalArgumentException("...");
?}
?this.filters.add(filter);
?return?this;
}

我們可以看到,F(xiàn)ilter 會(huì)被直接加到 List 中,而不是按照一定的順序來加入的。但同時(shí),我們也發(fā)現(xiàn)了一個(gè) comparator 對(duì)象,并且只有被注冊(cè)到了該類的 Filter 才能被加入到 filters 屬性中。這個(gè) comparator 又是用來做什么的呢?

在 Spring Security 創(chuàng)建過程中,會(huì)調(diào)用到 HttpSeciryt.performBuild 方法,在這里我們可以看到 filters 和 comparator 是如何被使用到的。

protected?DefaultSecurityFilterChain?performBuild()?throws?Exception?{
?Collections.sort(filters,?comparator);
?return?new?DefaultSecurityFilterChain(requestMatcher,?filters);
}

可以看到,Spring Security 使用了這個(gè) comparator 在獲取 SecurityFilterChain 的時(shí)候來保證 filters 的順序,所以,研究這個(gè) comparator 就能知道在 SecurityFilterChain 中的那些 Filter 的順序是如何的了。

這個(gè) comparator 的類型是 FilterComparator ,從名字就能看出來是專用于 Filter 比較的類,它的實(shí)現(xiàn)也并不神秘,從構(gòu)造函數(shù)就能猜到是如何實(shí)現(xiàn)的:

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 沒有攜帶認(rèn)證信息的問題。

小結(jié)

引入 Spring Security 之后,我們的 CORS 驗(yàn)證實(shí)際上是依然運(yùn)行著的,只是因?yàn)?preflight request 不會(huì)攜帶認(rèn)證信息,所以無法通過身份驗(yàn)證。使用 HttpSecurity.cors 方法會(huì)幫助我們?cè)诋?dāng)前的 Spring Context 中找到或創(chuàng)建一個(gè) CorsFilter 并安排在身份驗(yàn)證的 Filter 之前,以保證能對(duì) preflight request 正確處理。

總結(jié)

研究了 Spring 中 CORS 的代碼,我們了解到了這樣一些知識(shí):

  • 實(shí)現(xiàn) WebMvcConfigurer.addCorsMappings 方法來進(jìn)行的 CORS 配置,最后會(huì)在 Spring 的 Interceptor 或 Handler 中生效

  • 注入 CorsFilter 的方式會(huì)讓 CORS 驗(yàn)證在 Filter 中生效

  • 引入 Spring Security 后,需要調(diào)用 HttpSecurity.cors 方法以保證 CorsFilter 會(huì)在身份驗(yàn)證相關(guān)的 Filter之前執(zhí)行

  • HttpSecurity.cors + WebMvcConfigurer.addCorsMappings 是一種相對(duì)低效的方式,會(huì)導(dǎo)致跨域請(qǐng)求分別在 Filter 和 Interceptor 層各經(jīng)歷一次 CORS 驗(yàn)證

  • HttpSecurity.cors + 注冊(cè) CorsFilter 與 HttpSecurity.cors + 注冊(cè) CorsConfigurationSource 在運(yùn)行的時(shí)候是等效的

  • 在 Spring 中,沒有通過 CORS 驗(yàn)證的請(qǐng)求會(huì)得到狀態(tài)碼為 403 的響應(yīng)

點(diǎn)擊獲取?附送學(xué)習(xí)進(jìn)階架構(gòu)資料、PDF書籍文檔、面試資料

Spring 里那么多種 CORS 的配置方式,到底有什么區(qū)別

Spring 里那么多種 CORS 的配置方式,到底有什么區(qū)別


向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI