11.Spring security跨域问题

文章目录

      • *跨域问题*
        • *11.1什么是CORS*
        • *11.2Spring处理方案*
          • *11.2.1`@CrossOrigin`*
          • *11.2.2`addCorsMappings`*
          • *11.2.3`CorsFilter`*
        • *11.3Spring Security处理方案*
          • *11.3.1特殊处理`OPTIONS`请求*
          • *11.3.2继续使用`CorsFilter`*
          • *11.3.3专业解决方案*

跨域问题

11.1什么是CORS

CORS(Cross-Origin Resource Sharing)是由W3C制定的一种跨域资源共享技术标准,其目的就是为了解决前端的跨域请求
其中新增了一组HTTP请求头字段,通过这些字段,服务器告诉浏览器,哪些网站通过浏览器有权限访问哪些资源。同时规定,对那些可能修改服务器数据的HTTP请求方法(如GET以外的HTTP请求等),浏览器必须首先使用OPTIONS方法发起一个预检请求,预检请求的目的是查看服务端是否支持即将发起的跨域请求,如果服务端允许,才能发起实际的HTTP请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(如cookie、HTTP认证信息等)。

GET请求为例,如果需要发起一个跨域请求,则请求头如下:

Host: localhost:8080
Origin: http://localhost:8081
Referer: http://localhost:8081/index.html

如果服务端支持该跨域请求,那么返回的响应头中将包含:

Access-Control-Allow-Origin: http://localhost:8081

Access-Control-Allow-Origin字段用来告诉浏览器可以访问该资源的域,当浏览器收到这样的响应头信息之后,提取出Access-Control-Allow-Origin字段中的值,发现该值包含当前页面所在的域,就知道这个跨域是被允许的,因此就不再对前端的跨域请求进行限制。
这属于简单请求,即不需要进行预检请求的跨域。

对于一些非简单请求,会首先发送一个预检请求。类似于:

OPTIONS: /put HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: PUT
Origin: http://localhost:8081
Referer: http://localhost:8081/index.html
...

请求方法是OPTIONS,请求头Origin字段告诉服务端当前页面所在的域,请求头Access-Control-Request-Method告诉服务端即将发起的跨域请求所使用的方法。服务端对此进行判断,如果允许即将发起的跨域请求,则会给出如下响应:

HTTP/1.1 200
Access-Control-Allow-Origin: http://localhost:8081
Access-Control-Allow-Methods: PUT
Access-Control-Max-Age: 3600
...

Access-Control-Allow-Methods字段表示允许的跨域方法;Access-Control-Max-Age字段表示预检请求的有效期,单位为秒,在有效期内如果发起该跨域请求,则不用再次发起预检请求。预检请求结束后,接下来就会发起一个真正的跨域请求。

11.2Spring处理方案

11.2.1@CrossOrigin

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

@RestController
public class HelloController {
    @CrossOrigin(origins = "http://localhost:8081")
    @PostMapping("/post")
    public String post() {
        return "hello post";
    }
}

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

  • allowCredentials:浏览器是否应当发送凭证信息,如cookie。
  • allowedHeaders:请求被允许的请求头字段,*表示所有字段。
  • exposedHeaders:哪些响应头可以作为响应的一部分暴露出来。注意,这里只可以一一列举,通配符*在这里是无效的。
  • maxAge:预检请求的有效期,有效期内不必再次发送,默认是1800秒。
  • methods:允许的请求方法,*表示允许所有方法。
  • origins:允许的域,*表示允许所有域。

具体的执行过程:

  1. @CrossOrigin注解在AbstractHandlerMethodMapping的内部类MappingRegistryregister方法中完成解析的,@CrossOrigin注解中的内容会被解析成一个配置对象CorsConfiguration
  2. @CrossOrigin所标记的请求方法对象HandlerMethodCorsConfiguration一一对应存入一个名为corsLookupMap映射中。
  3. 当请求到达DispatcherServlet#doDispatch方法之后,调用AbstractHandlerMapping#getHandler方法获取执行链HandlerExecutionChain时,会从corsLookup映射中获取到CorsConfiguration对象。
  4. 根据获取到的CorsConfiguration对象构建一个CorsInterceptor拦截器。
  5. CorsInterceptor拦截器中触发对DefaultCorsProcessor#processRequest的调用,跨域请求的校验工作将在该方法中完成。
11.2.2addCorsMappings

有一种全局的配置方法,通过重写WebMvcConfigurerComposite#addCorsMappings方法来实现:

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        // addMapping表示要处理的请求地址
        registry.addMapping("/**")
            .allowedMethods("*")
            .allowedOrigins("*")
            .allowedHeaders("*")
            .allowCredentials(false)
            .exposedHeaders("")
            .maxAge(3600);
    }
}

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

  1. registry.addMapping("/**")方法配置了一个CorsRegistration对象,该对象中包含了一个路径拦截规则,拦截规则的值就是addMapping方法的参数,同时CorsRegistration中还包含了一个CorsConfiguration配置对象,该对象用来保存这里跨域相关的配置。
  2. WebMvcConfigurationSupport#requestMappingHandlerMapping方法中触发了addCorsMappings方法的执行,将获取到的CorsRegistration对象重新组装成一个UrlBasedCorsConfigurationSource对象,该对象中定义了一个corsConfigurations变量(Map),该变量保存了拦截器规则和CorsConfiguration对象的映射关系。
  3. 将新建的UrlBasedCorsConfigurationSource对象赋值给AbstractHandlerMapping#corsConfigurationSource属性。
  4. 当请求到达时的处理方法和@CrossOrigin注解处理流程的第3步一样,都是在AbstractHandlerMapping#getHandler方法中进行处理,不同的是,这里是从corsConfigurationSource中获取CorsConfiguration配置对象,而@CrossOrigin注解则从corsLookup映射中获取。如果两处都可以获取到,则对获取到的对象属性值进行合并。
  5. 根据获取到的CorsConfiguration对象构建一个CorsInterceptor拦截器。
  6. CorsInterceptor拦截器中触发对DefaultCorsProcessor#processRequest的调用,跨域请求的校验工作将在该方法中完成。

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

11.2.3CorsFilter

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

@Configuration
public class WebMvcConfig {
    /**
     * 由于是在spring boot项目中,这里通过FilterRegistrationBean来配置一个过滤器,这种配置方式既可以设置拦截规则,
     * 又可以为配置的过滤器设置优先级
     */
    @Bean
    FilterRegistrationBean<CorsFilter> corsFilter() {
        FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
        // 依然离不开CorsConfiguration对象,不同的是自己手动创建该对象,并逐个设置跨域的各项处理规则
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8081"));
        corsConfiguration.setMaxAge(3600L);
        // 还需要创建一个UrlBasedCorsConfigurationSource对象,将过滤器的拦截规则和CorsConfiguration
        // 对象之间的映射关系由UrlBasedCorsConfigurationSource中的corsConfigurations变量保存起来
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        // 创建一个CorsFilter,并为其配置一个优先级
        registrationBean.setFilter(new CorsFilter(source));
        registrationBean.setOrder(-1);
        return registrationBean;
    }
}

CorsFilter过滤器的doFilterInternal方法中,触发对DefaultCorsProcessor#processRequest的调用,进而完成跨域请求的校验。
和前面两种方式不同的是,CorsFilter是在过滤器中处理跨域的,而前面两种方案则是在DispatchServlet中触发跨域处理,从处理时间上来说,CorsFilter对于跨域的处理时机要早于前面两种。

这三种方式选择其中一种即可,不过需要说明的是:

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

11.3Spring Security处理方案

当为项目添加spring security依赖之后,通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域,会统统失效;通过CorsFilter配置的跨域,有没有失效则要看过滤器的优先级,如果过滤器优先级高于spring security过滤器,即先执行,则配置的跨域处理仍然有效;如果低于spring security过滤器的优先级,则失效。
为了理清楚这个问题,需要先简略了解一下FilterDispatcherServlet以及Interceptor执行顺序:

11.Spring security跨域问题_第1张图片

由于非简单请求都要首先发送一个预检请求,而预检请求并不会携带认证信息,所以预检请求就有被spring security拦截的可能。如果通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域,最终都是在CorsInterceptor中对跨域请求进行校验的。要进入CorsInterceptor拦截器,肯定要先过spring security过滤器链,而在经过过滤器链时,由于预检请求没有携带认证信息,就会被拦截下来。
如果使用了CorsFilter配置跨域,只要过滤器的优先级高于spring security过滤器,即在spring security过滤器之前就执行了跨域请求校验,那么就不会有问题。如果CorsFilter的优先级低于spring security过滤器,则预检请求一样需要先经过spring security的过滤器,由于没有携带认证信息,会被拦截下来。

11.3.1特殊处理OPTIONS请求

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

// 不推荐使用,既不安全,也不优雅
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                // 指定所有的OPTIONS请求直接通过
                .antMatchers(HttpMethod.OPTIONS).permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic()
                .and()
                .csrf().disable();
    }
}
11.3.2继续使用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://localhost:8081"));
    corsConfiguration.setMaxAge(3600L);
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfiguration);
    registrationBean.setFilter(new CorsFilter(source));
    registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return registrationBean;
}

过滤器的优先级,数字越小,优先级越高。
当然也可以不设置最高优先级,只需要了解到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过滤器的默认优先级)。

11.3.3专业解决方案

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 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);
        return source;
    }
}

cors()方法开启了对CorsConfigurer的配置,其最重要的方法就是configure方法:

public void configure(H http) {
    ApplicationContext context = http.getSharedObject(ApplicationContext.class);
    // 获取一个CorsFilter并添加到spring security过滤器链中
    CorsFilter corsFilter = getCorsFilter(context);
    http.addFilter(corsFilter);
}

// 一共有4种不同的方式获取CorsFilter
private CorsFilter getCorsFilter(ApplicationContext context) {
    // 1.如果configurationSource不为空,则直接根据它创建一个CorsFilter,前面的配置就是通过这种方式
    if (this.configurationSource != null) {
        return new CorsFilter(this.configurationSource);
    }
    // 2.判断spring容器中是否包含一个名为corsFilter的实例,如果包含,则取出并返回,意味着也可以直接向
    // 容器中注入一个corsFilter
    boolean containsCorsFilter = context.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
    if (containsCorsFilter) {
        return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
    }
    // 3.判断spring容器中是否包含一个名为corsConfigurationSource的实例,如果包含,则据此创建CorsFilter并返回
    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);
    }
    // 4.HandlerMappingIntrospector是spring web中提供的一个类,该类实现了CorsConfigurationSource接口,
    // 所以也可以据此创建一个CorsFilter
    boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR, context.getClassLoader());
    if (mvcPresent) {
        return MvcCorsFilter.getMvcCorsFilter(context);
    }
    return null;
}

拿到CorsFilter之后,调用http.addFilter方法将其添加到spring security过滤器链中,在过滤器链构建之前,会先对所有的过滤器进行排序,排序的依据在FilterOrderRegistration中已经定义好了:

FilterOrderRegistration() {
    Step order = new Step(INITIAL_ORDER, ORDER_STEP);
    put(ChannelProcessingFilter.class, order.next());
    order.next(); // gh-8105
    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根据开发者提供的CorsConfigurationSource对象构建出一个CorsFilter,并将该过滤器置于认证过滤器之前。
Spring security中关于跨域的这三种处理方式,在实际项目中推荐使用第三种。

你可能感兴趣的:(#,深入浅出spring,security,spring,java)