作为一个后端开发,我们经常遇到的一个问题就是需要配置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:一个布尔值,表示服务器是否允许使用cookiesAccess-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")
}
}CorsFilterCorsFilter同样来自于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就是/hellogetCorsConfigurations方法将保存的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来作为这次请求的handlerhandler,所以就在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方法,会经历:
HandlerMappinggetHandler方法得到HandlerExecutionChainCorsConfigurationSource的实例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)