Spring Security处理方案

当我们为项目添加了 Spring Security 依赖之后,发现上面三种跨域方式有的失效了,有的则可以继迷续使用,这是怎么回事?

通过 @CrossOrigin 注解或者重写 addCorsMappings 方法配置跨域,统统失效了;通过 CorsFilter 配置的跨域,有没有失效则要看过滤器的优先级,如果过滤器优先级高于 Spring Security 过滤器,即先于 Spring Security 过滤器执行,则 CorsFilter 所配置的跨域处理依然有效:如果过滤器优先级低于 Spring Security 过滤器,则 CorsFilter 所配置的跨域处理就会失效。

为了理清楚这问题,我们先简略了解一下 Filter、DispatchserServlet 以及 Interceptor 执行顺序。图 11-1 所示描述了请求从浏览器到达 Controller 的过程,Filter、Servlet 以及 Interceptor 执行顺序一目了然。

理清楚了执行顺序,我们再来看跨域请求过程。

image 2024 04 14 22 42 26 822
Figure 1. 图11-1 请求从浏览器达到 Controller

由于非简单请求都要首先发送一个预检请求(preflight request),而预检请求并不会携带认证信息,所以预检请求就有被 Spring Security 拦截的可能。如果通过 @CrossOrigin 注解或者重写 addCorsMappings 方法配置跨域,最终都是在 CorsInterceptor 中对跨域请求进行校验的。要进入 CorsInterceptor 拦截器,肯定要先过 Spring Security 过滤器链,而在经过 Spring Security 过滤器链时,由于预检请求没有携带认证信息,就会被拦截下来。

如果使用了 CorsFilter 配置跨域,只要过滤器的优先级高于 Spring Security 过滤器,即在 Spring Security 过滤器之前执行了跨域请求校验,那么就不会有问题。如果 CorsFilter 的优先级低于 Spring Security 过滤器,则预检请求一样需要先经过 Spring Security 过滤器,由于没有携带认证信息,在经过 Spring Security 过滤器时就会被拦截下来。

搞清楚了问题所在,接下来我们来看问题如何解决。

特殊处理 OPTIONS 请求

在引入 Spring Security 之后,如果还想继续通过 @CrossOrigin 注解或者重写 addCorsMappings 方法配置跨域,那么可以通过给 OPTIONS 请求单独放行,来解决预检请求被拦截的问题,具体配置如下:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and()
                .csrf().disable();
    }
}

在 configure(HttpSecurity) 方法中直接指定所有的 OPTIONS 请求直接通过。

这种方案既不安全,也不优雅,所以并不推荐在实际开发中使用,读者仅作了解即可。

继续使用 CorsFilter

第二种方案则是使用 CorsFilter 来处理跨域,只需要将 CorsFilter 的优先级设置高于 Spring Security 过滤器优先级,配置如下:

@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://local.javaboy.org:8081"));
    corsConfiguration.setMaxAge(10L);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);
    registrationBean.setFilter(new CorsFilter(source));
    registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return registrationBean;
}

过滤器的优先级,数字越小,优先级越高,这里我们配置了 CorsFilter 的优先级为最高。

当然这里也可以不设置最高优先级,我们只需要了解到 Spring Security 中 FilterChainProxy 过滤器的优先级,只要 CorsFilter 的优先级高于 FilterChainProxy 即可。

Spring Security 中关于 FilterChainProxy 优先级的配置在 SecurityFilterAutoConfiguration 类中,部分源码如下:

@Bean
@ConditionalOnBean(name = DEFAULT_FILTER_NAME)
public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration(
        SecurityProperties securityProperties) {
    DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean(
            DEFAULT_FILTER_NAME);
    registration.setOrder(securityProperties.getFilter().getOrder());
    registration.setDispatcherTypes(getDispatcherTypes(securityProperties));
    return registration;
}

可以看到,过滤器的优先级是从 SecurityProperties 对象中读取的,该对象中默认的过滤器优先级是 -100,即开发者配置的 CorsFilter 过滤器优先级只需要小于 -100 即可(开发者也可以在 application.properties 文件中,通过 spring.security.filter.order 配置去修改 FilterChainProxy 过滤器的默认优先级)。

专业解决方案

Spring Security 中也提供了更加专业的方式来解决预检请求所面临的问题。我们来看一下具体配置:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and()
                .cors()
                .configurationSource(corsConfigurationSource())
                .and()
                .csrf().disable();
    }

    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://local.javaboy.org:8081"));
        corsConfiguration.setMaxAge(10L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }
}

首先需要提供一个 CorsConfigurationSource 实例,将跨域的各项配置都填充进去,然后在 configure(HttpSecurity) 方法中,通过 cors() 开启跨域配置,并将一开始配置好的 CorsConfigurationSource 实例设置进去。这样我们就完成了 Spring Security 中的跨域配置。

那么,这段配置的原理是什么呢?cors() 方法开启了对 CorsConfigurer 的配置,对 CorsConfigurer 而言最重要的就是 configure 方法(回顾4.1.5小节),我们一起来看一下:

public void configure(H http) {
    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);
}

可以看到,configure 方法中就是获取了一个 CorsFilter 并添加到 Spring Security 过滤器链中。我们先来看 CorsFilter 是如何获取到的:

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;
}

可以看到,这里一共有四种不同的方式获取 CorsFilter:

  1. 如果 configurationSource 不为 null,则直接根据 configurationSource 创建一个 CorsFilter 对象并返回。我们前面的配置最终就是通过这种方式获取到 CorsFilter 实例的。

  2. Spring 容器中是否包含一个名为 corsFilter 的实例,如果包含,则从 Spring 容器中获取该实例并返回,这意味着我们也可以直接向 Spring 容器中注入一个 corsFilter。

  3. Spring 容器中是否包含一个名为 corsConfigurationSource 的实例,如果包含,则根据该实例创建一个 CorsFilter 并返回。这意味着在前面的配置中,我们可以将自己创建的 CorsConfigurationSource 实例直接注入到 Spring 容器中(添加 @Bean 注解即可),然后在 configure(HttpSecurity) 方法中通过 cors() 方法开启跨域配置即可,不用再手动指定 CorsConfigurationSource 实例。

  4. HandlerMappingIntrospector 是 Spring Web 中提供的一个类,该类实现了 CorsConfigurationSource 接口,所以也可以据此创建一个 CorsFilter。

这是四种获取 CorsFilter 实例的方式。

拿到 CorsFilter 之后,调用 http.addFilter 方法将其添加到 Spring Security 过滤器链中,在过滤器链构建之前,会先对所有的过滤器进行排序(回顾4.1.4小节关于 HttpSecurity 的讲解),排序的依据在 FilterComparator 中已经定义好了:

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());
    put(CsrfFilter.class, order.next());
    put(LogoutFilter.class, order.next());
    ....

可以看到,CorsFilter 的位置在 HeaderWriterFilter 之后,在 CsrfFilter 之前,这个时候还没到认证过滤器。

至此,Spring Security 中关于跨域问题的处理就清晰了,Spring Security 根据开发者提供的 CorsConfigurationSource 对象构建出一个 CorsFilter,并将该 CorsFilter 置于认证过滤器之前。

Spring Security 中关于跨域的这三种处理方式,在实际项目中推荐使用第三种。