前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring 里那么多种 CORS 的配置方式,到底有什么区别

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

作者头像
字母哥博客
发布2020-09-23 14:34:15
2.2K1
发布2020-09-23 14:34:15
举报

作为一个后端开发,我们经常遇到的一个问题就是需要配置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)

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CORS 是什么
    • 预检请求
      • 小结
      • 几种配置的方式
        • @CrossOrigin注解
          • 实现WebMvcConfigurer.addCorsMappings方法
            • 注入CorsFilter
              • Spring Security 中的配置
              • 这些配置有什么区别
                • Filter 与 Interceptor
                  • WebMvcConfigurer.addCorsMappings方法做了什么
                    • 注入 CORS 配置
                    • 获取 CORS 配置
                    • 小结
                  • HtttpSecurity.cors方法做了什么
                    • CorsConfigurer 如何配置 CORS 规则
                    • HttpSecurity 中的 filters 属性
                    • 小结
                • 总结
                相关产品与服务
                多因子身份认证
                多因子身份认证(Multi-factor Authentication Service,MFAS)的目的是建立一个多层次的防御体系,通过结合两种或三种认证因子(基于记忆的/基于持有物的/基于生物特征的认证因子)验证访问者的身份,使系统或资源更加安全。攻击者即使破解单一因子(如口令、人脸),应用的安全依然可以得到保障。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档