Fork me on GitHub
16 February 2020

目录:

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 请求处理流程简图:

FrameworkServletFrameworkServletDispatcherServletDispatcherServletHandlerMappingHandlerMappingHandlerAdapterHandlerAdapterdoOptionsdoServicedoDispatchgetHandlergetHandlerHandlerExecutionChaingetHandlerAdapterhandle

当跨域时的预检请求(OPTIONS)发生时,HandlerMapping.getHandler 便会返回 PreFlightHandler 作为本次请求的”handler”,它实现了 HttpRequestHandler 接口,因此由 HttpRequestHandlerAdapter处理。

调用流程简图:

PreFlightHandlerPreFlightHandlerCorsProcessorCorsProcessorCorsConfigurationCorsConfigurationhandleRequestprocessRequestcheckOrigin,checkHeaders...

从上已经了解到了基本调用流程,那具体代码是如何执行的呢?

AbstractHandlerMappingHandlerMapping 的抽象类实现,“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;
}

除了可以通过 PreFlightHandlerCorsFilterCrossOrigin 处理跨域外,框架还提供了 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);
}