Spring处理方案

首先回顾一下 Spring 中关于跨域的处理方案,这有助于我们理解 Spring Security 中的跨域。Spring 中对于跨域的处理一共有三种不同的方案,我们分别来看。

@CrossOrigin

Spring 中第一种处理跨域的方式是通过 @CrossOrigin 注解来标记支持跨域,该注解可以添加在方法上,也可以添加在 Controller 上。当添加在 Controller 上时,表示 Controller 中的所有接口都支持跨域,具体配置如下:

@RestController
public class HelloController {
    @PostMapping("/post")
    public String post() {
        return "hello post";
    }
}

@CrossOrigin 注解各属性含义如下:

  • allowCredentials:浏览器是否应当发送凭证信息,如 Cookie。

  • allowedHeaders:请求被允许的请求头字段,* 表示所有字段。

  • exposedHeaders:哪些响应头可以作为响应的一部分暴露出来。注意,这里只可以一一列举,通配符 * 在这里是无效的。

  • maxAge:预检请求的有效期,有效期内不必再次发送预检请求,默认是 1800 秒。

  • methods:允许的请求方法,* 表示允许所有方法。

  • origins:允许的域,* 表示允许所有域。

该注解的实现原理属于 Spring 范畴,这里不做过多介绍,本书仅和大家简单梳理一下 @CrossOrigin 注解执行过程,这有助于我们理解后面的内容(读者可按照下面的叙述自行 Debug 以加深理解)。

  1. @CrossOrigin 注解在 AbstractHandlerMethodMapping 的内部类 MappingRegistry 的 register 方法中完成解析的,@CrossOrigin 注解中的内容会被解析成一个配置对象 CorsCon figuration。

  2. 将 @CrossOrigin 所标记的请求方法对象 HandlerMethod 和 CorsConfiguration 一一对应存入一个名为 corsLookup 的 Map 集合中。

  3. 当请求到达 DispatcherServlet#doDispatch 方法之后,调用 AbstractHandlerMapping#getHandler 方法获取执行链 HandlerExecutionChain 时,会从 corsLookup 集合中获取到 CorsConfiguration 对象。

  4. 根据获取到的 CorsConfiguration 对象构建一个 CorsInterceptor 拦截器。

  5. 在 CorsInterceptor 拦截器中触发对 DefaultCorsProcessor#processRequest 的调用,跨域请求的校验工作将在该方法中完成。

addCorsMappings

@CrossOrigin 注解需要添加在不同的 Controller 上。所以还有一种全局的配置方法,就是通过重写 WebMvcConfigurerComposite#addCorsMappings 方法来实现,具体配置如下:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("*")
                .allowedOrigins("*")
                .allowedHeaders("*")
                .allowCredentials(false)
                .exposedHeaders("")
                .maxAge(3600);
    }
}

addMapping 表示要处理的请求地址,接下来的方法含义和 @CrossOrigin 注解中属性的含义都一一对应,这里不再赘述。

这种配置方式最终的处理方式和 @CrossOrigin 注解相同,都是在 CorsInterceptor 拦截器中触发对 DefaultCorsProcessor#processRequest 的调用,并最终在该方法中完成对跨域请求的校验工作。不过在源码执行过程中略有差异。

  1. registry.addMapping("/**") 方法配置了一个 CorsRegistration 对象,该对象中包含了一个路径拦截规则,拦截规则的值就是 addMapping 方法的参数,同时 CorsRegistration 中还包含了一个 CorsConfiguration 配置对象,该对象用来保存这里跨域相关的配置。

  2. 在 WebMvcConfigurationSupport#requestMappingHandlerMapping 方法中触发了 addCorsMappings 方法的执行,将获取到的 CorsRegistration 对象重新组装成一个 UrlBasedCorsConfigurationSource 对象,该对象中定义了一个 corsConfigurations 变量(Map<String,CorsConfiguration>),该变量保存了拦截规则和 CorsConfiguration 对象的映射关系。

  3. 将新建的 UrlBasedCorsConfigurationSource 对象赋值给 AbstractHandlerMapping#corsConfigurationSource 属性。

  4. 当请求到达时的处理方法和 @CrossOrigin 注解处理流程的第 3 步一样,都是在 AbstractHandlerMapping#getHandler 方法中进行处理,不同的是,这里是从 corsConfigurationSource 中获取 CorsConfiguration 配置对象,而 @CrossOrigin 注解则从 corsLookup 集合中获取到 CorsConfiguration 配置对象。如果两处都可以获取到 CorsConfiguration 对象,则对获取到的对象属性值进行合并。

  5. 根据获取到的 CorsConfiguration 对象构建一个 CorsInterceptor 拦截器。

  6. 在 CorsInterceptor 拦截器中触发对 DefaultCorsProcessor#processRequest 的调用,跨域请求的校验工作将在该方法中完成。

这两种跨域配置方式殊途同归,最终目的都是配置了一个 CorsConfiguration 对象,并根据该对象创建 CorsInterceptor 拦截器,然后在 CorsInterceptor 拦截器中触发 DefaultCorsProcessor#processRequest 方法的执行,完成跨域的校验。另外还需要注意的是,这里的跨域校验是由 DispatcherServlet 中的方法触发的,而 DispatcherServlet 的执行是在 Filter 之后,这一点需要牢记,我们后面将会用到。

CorsFilter

CorsFilter 是 Spring Web 中提供的一个处理跨域的过滤器,开发者也可以通过该过滤器处理跨域:

@Configuration
public class WebMvcConfig {
    @Bean
    FilterRegistrationBean<CorsFilter> corsFilter() {
        FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        registrationBean.setFilter(new CorsFilter(source));
        registrationBean.setOrder(-1);
        return registrationBean;
    }
}

CorsFilter 过滤器的配置也很容易:

  • 由于是在 Spring Boot 项目中,这里通过 FilterRegistrationBean 来配置一个过滤器,这种配置方式既可以设置拦截规则,又可以为配置的过滤器设置优先级。

  • 在这里依然离不开 CorsConfiguration 对象,不同的是我们自己手动创建该对象,并逐个设置跨域的各项处理规则。

  • 我们还需要创建一个 UrlBasedCorsConfigurationSource 对象,将过滤器的拦截规则和 CorsConfiguration 对象之间的映射关系由 UrlBasedCorsConfigurationSource 中的 corsConfigurations 变量保存起来。

  • 最后创建一个 CorsFilter,并为其配置一个优先级。

在 CorsFilter 过滤器的 doFilterlnternal 方法中,触发对 DefaultCorsProcessor#processRequest 的调用,进而完成跨域请求的校验。

和前面两种方式不同的是,CorsFilter 是在过滤器中处理跨域的,而前面两种方案则是在 DispatcherServlet 中触发跨域处理,从处理时间上来说,CorsFilter 对于跨域的处理时机要早于前面两种。

这就是 Spring 中为我们提供的三种不同的跨域解决方案,三种方式都能解决问题,选择其中任意一种即可。需要说明的是:

  • @CrossOrigin 注解+重写 addCorsMappings 方法同时配置,这两种方式中关于跨域的配置会自动合并,跨域在 CorsInterceptor 中只处理了一次。

  • @CrossOrigin 注解 + CorsFilter 同时配置,或者重写 addCorsMappings 方法 + CorsFilter 同时配置,都会导致跨域在 CorsInterceptor 和 CorsFilter 中各处理了一次,降低程序运行效率,这种组合不可取。