Spring CORS 源码解析
目录:
1. CORS 是什么
CORS
全称为 Cross-Origin Resource Sharing,为了安全浏览器会限制非同源的请求除非该请求得到允许。对于简单请求只要服务器返回正确的响应头即可,但是有些请求需要先进行预检请求,要求必须首先使用 OPTIONS
方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。
跨域资源共享(CORS) 是一种机制,它使用额外的 HTTP 头来告诉浏览器 让运行在一个 origin (domain) 上的Web应用被准许访问来自不同源服务器上的指定的资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。 跨域资源共享( CORS )机制允许 Web 应用服务器进行跨域访问控制,从而使跨域数据传输得以安全进行。现代浏览器支持在 API 容器中(例如 XMLHttpRequest 或 Fetch )使用 CORS,以降低跨域 HTTP 请求所带来的风险。
2. Spring web MVC 如何处理跨域
web 请求处理流程简图:
当跨域时的预检请求(OPTIONS)发生时,HandlerMapping.getHandler
便会返回 PreFlightHandler
作为本次请求的”handler”,它实现了 HttpRequestHandler
接口,因此由 HttpRequestHandlerAdapter
处理。
调用流程简图:
从上已经了解到了基本调用流程,那具体代码是如何执行的呢?
AbstractHandlerMapping
,HandlerMapping
的抽象类实现,“Spring web MVC” 中的大部分 HandlerMapping
实现类都是继承本类,因此跨域处理相关代码从本类开始。
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
Object handler = getHandlerInternal(request);
...
// 如果是跨域预检请求则创建对应的 HandlerExecutionChain
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
// 如果配置了 corsConfigurationSource 则根据它获取 CorsConfiguration
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(request) : null);
// 获取 handler 携带的 CorsConfiguration
//
// 注意:
// AbstractHandlerMethodMapping 定义 initCorsConfiguration 方法, 以及由内部类 MappingRegistry
// 负责处理和缓存 initCorsConfiguration 方法返回的配置,
// 然后在 getCorsConfiguration 方法中把该配置和父类该方法返回的配置进行合并,
// 可通过 WebMvcConfigurer.addCorsMappings 方法进行配置 MappingRegistry
// 子类 RequestMappingHandlerMapping 在 initCorsConfiguration 方法中处理 CrossOrigin 注解并创建对应的 CorsConfiguration
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
// 合并跨域配置
config = (config != null ? config.combine(handlerConfig) : handlerConfig);
// 配置 HandlerExecutionChain
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
// 配置 HandlerExecutionChain
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
// 如果是预检请求的话创建 HandlerExecutionChain
chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
else {
// 如果不是预检请求,则添加 CorsInterceptor
// CorsInterceptor 同样也通过 CorsProcessor 处理跨域请求,因此本文不做过多说明
chain.addInterceptor(0, new CorsInterceptor(config));
}
return chain;
}
CorsProcessor
默认实现类为 DefaultCorsProcessor
,根据 CorsConfiguration
进行请求处理,基本上对 CorsConfiguration
进行封装以及添加跨域相关响应头等方面的处理,具体的跨域相关检查由 CorsConfiguration
完成,下面简单分析 CorsConfiguration
对“origin”的检查,其它同理就不做过多介绍,感兴趣的朋友可直接查看源码。
// 检查 origin, 返回 null 表示检查不通过
public String checkOrigin(@Nullable String requestOrigin) {
if (!StringUtils.hasText(requestOrigin)) {
return null;
}
// 配置的 allowedOrigins 是否为 null
if (ObjectUtils.isEmpty(this.allowedOrigins)) {
return null;
}
// allowedOrigins 为 * 时, 如果允许请求携带 cookie, 则返回请求来源地址, 否则返回 *
// 发出请求时,如果前端携带了 cookie, 而服务器配置为 *, 浏览器则会拒绝请求
if (this.allowedOrigins.contains(ALL)) {
if (this.allowCredentials != Boolean.TRUE) {
return ALL;
}
else {
return requestOrigin;
}
}
// 遍历配置的 allowedOrigins, 如果包含请求来源地址则返回该地址
for (String allowedOrigin : this.allowedOrigins) {
if (requestOrigin.equalsIgnoreCase(allowedOrigin)) {
return requestOrigin;
}
}
return null;
}
除了可以通过 PreFlightHandler
,CorsFilter
和 CrossOrigin
处理跨域外,框架还提供了 CorsFilter
来处理跨域请求,该类在默认情况下并未启用,不过 Spring Security
启用了该类,详情参考下文所述。
3. Spring Security 如何处理跨域
使用 Spring Security
时一般会对 HttpSecurity
进行配置,如下所示:
@Configuration
@EnableWebSecurity
public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**")
.permitAll()
.and()
// 允许跨域
.cors();
}
}
如此简单方法就配置好允许跨域了,其中到底发生了什么,让我们一探究竟。
cors
方法代码如下:
public CorsConfigurer<HttpSecurity> cors() throws Exception {
return getOrApply(new CorsConfigurer<>());
}
调用cors
方法时会获取到CorsConfigurer
,该类负责创建和配置 CorsFilter
。
// 它实现 SecurityConfigurer 接口定义的方法
// SecurityBuilder 在 build 时会调用,参考 AbstractConfiguredSecurityBuilder
public void configure(H http) {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
// 获取 CorsFilter
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.");
}
// 添加 CorsFilter 到 filter 调用链
http.addFilter(corsFilter);
}
private CorsFilter getCorsFilter(ApplicationContext context) {
if (this.configurationSource != null) {
// 根据 corsConfigurationSource 创建
return new CorsFilter(this.configurationSource);
}
// 判断容器中是否包含名字为 corsFilter 的实例
// 如果包含则直接返回
boolean containsCorsFilter = context
.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
if (containsCorsFilter) {
return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
}
// 判断容器中是否包含名字为 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);
}
// 判断当前类加载器是否加载 org.springframework.web.servlet.handler.HandlerMappingIntrospector 类
// 如果是则获取 HandlerMappingIntrospector 实例,并根据它创建 CorsFilter
boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR,
context.getClassLoader());
if (mvcPresent) {
return MvcCorsFilter.getMvcCorsFilter(context);
}
return null;
}
从以上代码可以了解到四种获取 CorsFilter
的方式:
- 根据配置的
CorsConfigurationSource
创建 - 从容器中读取
CorsFilter
- 从容器中读取
CorsConfigurationSource
并以此创建 - 从容器中读取
HandlerMappingIntrospector
并以此创建
默认使用第四种方式,该类可调用”MVC”中对跨域的配置,如通过 WebMvcConfigurer.addCorsMappings
所做的配置。如果想要自定义配置的话使用第三种方式即创建 CorsConfigurationSource
最为简单。
以上四种处理方式都是通过 CorsFilter
进行跨域处理,它也是调用 CorsProcessor.processRequest
来处理的,和前文所述相同就不详细说明来。它的构造方法只有一个参数,为 CorsConfigurationSource
类型。CorsConfigurationSource
负责从请求中读取对应的“CorsConfiguration”,应用中常用的实现类为UrlBasedCorsConfigurationSource
,该类可根据路径来配置,如 source.registerCorsConfiguration("/**", configuration)
,把自定义的”configuration”注册到全局路径。
private final CorsConfigurationSource configSource;
private CorsProcessor processor = new DefaultCorsProcessor();
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 获取 CorsConfiguration
CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);
// 检查请求
boolean isValid = this.processor.processRequest(corsConfiguration, request, response);
// 如果检查不符合跨域规范则直接返回中断本次请求
if (!isValid || CorsUtils.isPreFlightRequest(request)) {
return;
}
filterChain.doFilter(request, response);
}