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
字段表示预检请求的有效期,单位为秒,在有效期内如果发起该跨域请求,则不用再次发起预检请求。预检请求结束后,接下来就会发起一个真正的跨域请求。
@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
:允许的域,*
表示允许所有域。具体的执行过程:
@CrossOrigin
注解在AbstractHandlerMethodMapping
的内部类MappingRegistry
的register
方法中完成解析的,@CrossOrigin
注解中的内容会被解析成一个配置对象CorsConfiguration
。- 将
@CrossOrigin
所标记的请求方法对象HandlerMethod
和CorsConfiguration
一一对应存入一个名为corsLookup
的Map
映射中。- 当请求到达
DispatcherServlet#doDispatch
方法之后,调用AbstractHandlerMapping#getHandler
方法获取执行链HandlerExecutionChain
时,会从corsLookup
映射中获取到CorsConfiguration
对象。- 根据获取到的
CorsConfiguration
对象构建一个CorsInterceptor
拦截器。- 在
CorsInterceptor
拦截器中触发对DefaultCorsProcessor#processRequest
的调用,跨域请求的校验工作将在该方法中完成。
addCorsMappings
有一种全局的配置方法,通过重写
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
的调用,并最终在该方法中完成对跨域请求的校验工作,不过在源码执行过程中略有差异。
registry.addMapping("/**")
方法配置了一个CorsRegistration
对象,该对象中包含了一个路径拦截规则,拦截规则的值就是addMapping
方法的参数,同时CorsRegistration
中还包含了一个CorsConfiguration
配置对象,该对象用来保存这里跨域相关的配置。- 在
WebMvcConfigurationSupport#requestMappingHandlerMapping
方法中触发了addCorsMappings
方法的执行,将获取到的CorsRegistration
对象重新组装成一个UrlBasedCorsConfigurationSource
对象,该对象中定义了一个corsConfigurations
变量(Map
),该变量保存了拦截器规则和CorsConfiguration
对象的映射关系。- 将新建的
UrlBasedCorsConfigurationSource
对象赋值给AbstractHandlerMapping#corsConfigurationSource
属性。- 当请求到达时的处理方法和
@CrossOrigin
注解处理流程的第3步一样,都是在AbstractHandlerMapping#getHandler
方法中进行处理,不同的是,这里是从corsConfigurationSource
中获取CorsConfiguration
配置对象,而@CrossOrigin
注解则从corsLookup
映射中获取。如果两处都可以获取到,则对获取到的对象属性值进行合并。- 根据获取到的
CorsConfiguration
对象构建一个CorsInterceptor
拦截器。- 在
CorsInterceptor
拦截器中触发对DefaultCorsProcessor#processRequest
的调用,跨域请求的校验工作将在该方法中完成。
这两种跨域配置方式殊途同归,最终目的都是配置了一个
CorsConfiguration
对象,并根据该对象创建CorsInterceptor
拦截器,然后在拦截器中触发DefaultCorsProcessor#processRequest
方法的执行,完成跨域的校验。
另外还需要注意的是,这里的跨域校验是由DispatchServlet
中的方法触发的,而DispatchServlet
的执行是在Filter
之后,这一点需要牢记。
CorsFilter
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
同时配置,都会导致跨域在CorsInterceptor
和CorsFilter
中各处理一次,降低程序运行效率,这种组合不可取。
当为项目添加spring security依赖之后,通过
@CrossOrigin
注解或者重写addCorsMappings
方法配置跨域,会统统失效;通过CorsFilter
配置的跨域,有没有失效则要看过滤器的优先级,如果过滤器优先级高于spring security过滤器,即先执行,则配置的跨域处理仍然有效;如果低于spring security过滤器的优先级,则失效。
为了理清楚这个问题,需要先简略了解一下Filter
、DispatcherServlet
以及Interceptor
执行顺序:
由于非简单请求都要首先发送一个预检请求,而预检请求并不会携带认证信息,所以预检请求就有被spring security拦截的可能。如果通过
@CrossOrigin
注解或者重写addCorsMappings
方法配置跨域,最终都是在CorsInterceptor
中对跨域请求进行校验的。要进入CorsInterceptor
拦截器,肯定要先过spring security过滤器链,而在经过过滤器链时,由于预检请求没有携带认证信息,就会被拦截下来。
如果使用了CorsFilter
配置跨域,只要过滤器的优先级高于spring security过滤器,即在spring security过滤器之前就执行了跨域请求校验,那么就不会有问题。如果CorsFilter
的优先级低于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()
// 指定所有的OPTIONS请求直接通过
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated()
.and()
.httpBasic()
.and()
.csrf().disable();
}
}
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
过滤器的默认优先级)。
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中关于跨域的这三种处理方式,在实际项目中推荐使用第三种。