博主自主知识产权《Spring Boot深入浅出系列课程》(16章97节文档) 已经上线,请关注

Spring 里那么多种 CORS 的配置方式,到底有什么区别

spring 字母哥 0评论

作为一个后端开发,我们经常遇到的一个问题就是需要配置CORS,好让我们的前端能够访问到我们的 API,并且不让其他人访问。而在Spring中,我们见过很多种CORS的配置,很多资料都只是告诉我们可以这样配置、可以那样配置,但是这些配置有什么区别?

CORS 是什么

首先我们要明确,CORS是什么,以及规范是如何要求的。这里只是梳理一下流程,具体的规范请看这里

CORS全称是Cross-Origin Resource Sharing,直译过来就是跨域资源共享。要理解这个概念就需要知道资源同源策略这三个概念。

  • 域,指的是一个站点,由protocalhostport三部分组成,其中host可以是域名,也可以是ipport如果没有指明,则是使用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:实际请求中可以使用到的方法集合

浏览器会根据预检请求的响应,来决定是否发起实际请求。

小结

到这里, 我们就知道了跨域请求会经历的故事:

  1. 访问另一个域的资源
  2. 有可能会发起一次预检请求(非简单请求,或超过了Max-Age
  3. 发起实际请求

接下来,我们看看在 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的,但在进行之前,我们要先复习一下FilterInterceptor的概念。

Filter 与 Interceptor

上图很形象的说明了FilterInterceptor的区别,一个作用在DispatcherServlet调用前,一个作用在调用后。

但实际上,它们本身并没有任何关系,是完全独立的概念。

FilterServlet标准定义,要求Filter需要在Servlet被调用之前调用,作用顾名思义,就是用来过滤请求。在Spring Web应用中,DispatcherServlet就是唯一的Servlet实现。

Interceptor由 Spring 自己定义,由DispatcherServlet调用,可以定义在Handler调用前后的行为。这里的Handler,在多数情况下,就是我们的Controller中对应的方法。

对于FilterInterceptor的复习就到这里,我们只需要知道它们会在什么时候被调用到,就能理解后面的内容了。

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 默认为GETHEADPOST
  • allowedHeaders 默认为所有
  • maxAge 默认为 30 分钟
  • exposedHeaders 默认为 null,也就是不暴露任何 header
  • credentials 默认为 null

创建CorsRegistration后,我们可以通过它的allowedOriginsallowedMethods等方法修改它的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 配置

还记得前面关于FilterInterceptor那张图吗?当请求来到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验证

这里的PreFlightHandlerCorsInterceptor都是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 SecurityCORS就失效了,直到调用这个方法后,CORS规则才重新生效。

下面这些原因,导致了preflight request无法通过身份验证,从而导致CORS失效:

  1. preflight request不会携带认证信息
  2. Spring Security通过Filter来进行身份验证
  3. InterceptorHttpRequestHanlderDispatcherServlet之后被调用
  4. 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 SecurityFilters之前调用。但是在研究这个之前,我们先来看看同样重要的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,如果有的话,就用它创建一个CorsFilter
  2. 在当前的上下文中,是否存在一个名为corsFilter的实例,如果有的话,就把他当作一个CorsFilter来用。
  3. 在当前的上下文中,是否存在一个名为corsConfigurationSourceCorsConfigurationSource实例,如果有的话,就用它创建一个CorsFilter
  4. 在当前上下文的类加载器中,是否存在类HandlerMappingIntrospector,如果有的话,则通过MvcCorsFilter这个内部类创建一个CorsFilter
  5. 如果没有找到,那就返回一个null,调用的地方最后会抛出异常,阻止 Spring 初始化。

上面的第 2、3、4 步能解答我们前面的配置为什么生效,以及它们的区别。

注册CorsFilter的方式,这个Filter最终会被直接注册到 Servlet container 中被使用到。

注册CorsConfigurationSource的方式,会用这个source创建一个CorsFiltet然后注册到 Servlet container 中被使用到。

而第四步的情况比较复杂。HandlerMappingIntrospectorSpring Web提供的一个类,实现了CorsConfigurationSource接口,所以在MvcCorsFilter中,它被直接用于创建CorsFilter。它实现的getCorsConfiguration方法,会经历:

  1. 遍历HandlerMapping
  2. 调用getHandler方法得到HandlerExecutionChain
  3. 从中找到CorsConfigurationSource的实例
  4. 调用这个实例的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方法,在这里我们可以看到filterscomparator是如何被使用到的。


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 的InterceptorHandler中生效
  • 注入CorsFilter的方式会让CORS验证在Filter中生效
  • 引入Spring Security后,需要调用HttpSecurity.cors方法以保证CorsFilter会在身份验证相关的Filter之前执行
  • HttpSecurity.cors+WebMvcConfigurer.addCorsMappings是一种相对低效的方式,会导致跨域请求分别在FilterInterceptor层各经历一次CORS验证
  • HttpSecurity.cors+ 注册CorsFilterHttpSecurity.cors+ 注册CorsConfigurationSource在运行的时候是等效的
  • 在 Spring 中,没有通过CORS验证的请求会得到状态码为 403 的响应
喜欢 (4)or分享 (0)
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址