作为一个后端开发,我们经常遇到的一个问题就是需要配置CORS
,好让我们的前端能够访问到我们的 API,并且不让其他人访问。而在Spring
中,我们见过很多种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
之后,我们会发现前面的方法都不能正确的配置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
的区别,一个作用在DispatcherServlet
调用前,一个作用在调用后。
但实际上,它们本身并没有任何关系,是完全独立的概念。
Filter
由Servlet
标准定义,要求Filter
需要在Servlet
被调用之前调用,作用顾名思义,就是用来过滤请求。在Spring Web
应用中,DispatcherServlet
就是唯一的Servlet
实现。
Interceptor
由 Spring 自己定义,由DispatcherServlet
调用,可以定义在Handler
调用前后的行为。这里的Handler
,在多数情况下,就是我们的Controller
中对应的方法。
对于Filter
和Interceptor
的复习就到这里,我们只需要知道它们会在什么时候被调用到,就能理解后面的内容了。
WebMvcConfigurer.addCorsMappings
方法做了什么我们从WebMvcConfigurer.addCorsMappings
方法的参数开始,先看看CORS
配置是如何保存到 Spring 上下文中的,然后在了解一下 Spring 是如何使用的它们。
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
规则:
GET
、HEAD
和POST
创建CorsRegistration
后,我们可以通过它的allowedOrigins
、allowedMethods
等方法修改它的CorsConfiguration
,覆盖掉上面的默认值。
现在,我们已经通过WebMvcConfigurer.addCorsMappings
方法配置好CorsRegistry
了,接下来看看这些配置会在什么地方被注入到 Spring 上下文中。
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 容器中,当请求来临时,将会被调用。
还记得前面关于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
方法是如何解决这个问题的。
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
。
也许你会好奇,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
的方式降低了我们代码的效率,也许微乎其微,但能避免的情况下,还是不要使用。
在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
在运行的时候是等效的CORS
验证的请求会得到状态码为 403 的响应喜欢 (4)or分享 (0)