*跨域问题*
*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什么是CORSCORS(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: *
@Bean
FilterRegistrationBean corsFilter() {
FilterRegistrationBean 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对于跨域的处理时机要早于前面两种。
11.3Spring Security处理方案这三种方式选择其中一种即可,不过需要说明的是:
@CrossOrigin注解 + 重写addCorsMappings方法同时配置,这两种方式中关于跨域的配置会自动合并,跨域在CorsInterceptor中只处理了一次。@CrossOrigin注解 + CorsFilter同时配置,或者重写addCorsMappings方法 + CorsFilter同时配置,都会导致跨域在CorsInterceptor和CorsFilter中各处理一次,降低程序运行效率,这种组合不可取。
当为项目添加spring security依赖之后,通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域,会统统失效;通过CorsFilter配置的跨域,有没有失效则要看过滤器的优先级,如果过滤器优先级高于spring security过滤器,即先执行,则配置的跨域处理仍然有效;如果低于spring security过滤器的优先级,则失效。
为了理清楚这个问题,需要先简略了解一下Filter、DispatcherServlet以及Interceptor执行顺序:
11.3.1特殊处理OPTIONS请求由于非简单请求都要首先发送一个预检请求,而预检请求并不会携带认证信息,所以预检请求就有被spring security拦截的可能。如果通过@CrossOrigin注解或者重写addCorsMappings方法配置跨域,最终都是在CorsInterceptor中对跨域请求进行校验的。要进入CorsInterceptor拦截器,肯定要先过spring security过滤器链,而在经过过滤器链时,由于预检请求没有携带认证信息,就会被拦截下来。
如果使用了CorsFilter配置跨域,只要过滤器的优先级高于spring security过滤器,即在spring security过滤器之前就执行了跨域请求校验,那么就不会有问题。如果CorsFilter的优先级低于spring security过滤器,则预检请求一样需要先经过spring security的过滤器,由于没有携带认证信息,会被拦截下来。
在引入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 FilterRegistrationBeancorsFilter() { FilterRegistrationBean 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;
}
11.3.3专业解决方案可以看到,过滤器的优先级是从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 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中关于跨域的这三种处理方式,在实际项目中推荐使用第三种。



