`

spring MVC cors跨域实现源码解析

 
阅读更多

昨天前端遇到跨域问题,但是这个问题,我应该在很早就配置过,后来发现,spring boot的配置有一点改变。

查找了一些资料,才发现是拦截器的顺序问题。

pring MVC cors跨域实现源码解析

名词解释:跨域资源共享(Cross-Origin Resource Sharing)

简单说就是只要协议、IP、http方法任意一个不同就是跨域。

spring MVC自4.2开始添加了跨域的支持。

跨域具体的定义请移步mozilla查看

使用案例

spring mvc中跨域使用有3种方式:

在web.xml中配置CorsFilter

<filter>
  <filter-name>cors</filter-name>
  <filter-class>org.springframework.web.filter.CorsFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>cors</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

在xml中配置

// 简单配置,未配置的均使用默认值,就是全面放开
<mvc:cors>  
    <mvc:mapping path="/**" />  
</mvc:cors> 

// 这是一个全量配置
<mvc:cors>  
    <mvc:mapping path="/api/**"  
        allowed-origins="http://domain1.com, http://domain2.com"  
        allowed-methods="GET, PUT"  
        allowed-headers="header1, header2, header3"  
        exposed-headers="header1, header2" allow-credentials="false"  
        max-age="123" />  
  
    <mvc:mapping path="/resources/**"  
        allowed-origins="http://domain1.com" />  
</mvc:cors>  

使用注解

@CrossOrigin(maxAge = 3600)  
@RestController  
@RequestMapping("/account")  
public class AccountController {  
  
    @CrossOrigin("http://domain2.com")  
    @RequestMapping("/{id}")  
    public Account retrieve(@PathVariable Long id) {  
        // ...  
    }  
}  

涉及概念

  • CorsConfiguration 具体封装跨域配置信息的pojo

  • CorsConfigurationSource request与跨域配置信息映射的容器

  • CorsProcessor 具体进行跨域操作的类

  • 诺干跨域配置信息初始化类

  • 诺干跨域使用的Adapter

涉及的java类:

  • 封装信息的pojo

    CorsConfiguration

  • 存储request与跨域配置信息的容器

    CorsConfigurationSource、UrlBasedCorsConfigurationSource

  • 具体处理类

    CorsProcessor、DefaultCorsProcessor

  • CorsUtils

  • 实现OncePerRequestFilter接口的Adapter

    CorsFilter

  • 校验request是否cors,并封装对应的Adapter

    AbstractHandlerMapping、包括内部类PreFlightHandler、CorsInterceptor

  • 读取CrossOrigin注解信息

    AbstractHandlerMethodMapping、RequestMappingHandlerMapping

  • 从xml文件中读取跨域配置信息

    CorsBeanDefinitionParser

  • 跨域注册辅助类

    MvcNamespaceUtils

debug分析

要看懂代码我们需要先了解下封装跨域信息的pojo--CorsConfiguration

这边是一个非常简单的pojo,除了跨域对应的几个属性,就只有combine、checkOrigin、checkHttpMethod、checkHeaders。

属性都是多值组合使用的。

    // CorsConfiguration
    public static final String ALL = "*";
    // 允许的请求源
    private List<String> allowedOrigins;
    // 允许的http方法
    private List<String> allowedMethods;
    // 允许的请求头
    private List<String> allowedHeaders;
    // 返回的响应头
    private List<String> exposedHeaders;
    // 是否允许携带cookies
    private Boolean allowCredentials;
    // 预请求的存活有效期
    private Long maxAge;

combine是将跨域信息进行合并

3个check方法分别是核对request中的信息是否包含在允许范围内

配置初始化

在系统启动时通过CorsBeanDefinitionParser解析配置文件;

加载RequestMappingHandlerMapping时,通过InitializingBean的afterProperties的钩子调用initCorsConfiguration初始化注解信息;

配置文件初始化

在CorsBeanDefinitionParser类的parse方法中打一个断点。

CorsBeanDefinitionParser中的断点

CorsBeanDefinitionParser的调用栈

CorsBeanDefinitionParser的调用栈

通过代码可以看到这边解析中的定义信息。

跨域信息的配置可以以path为单位定义多个映射关系。

解析时如果没有定义则使用默认设置

// CorsBeanDefinitionParser
if (mappings.isEmpty()) {
    // 最简配置时的默认设置
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowedOrigins(DEFAULT_ALLOWED_ORIGINS);
    config.setAllowedMethods(DEFAULT_ALLOWED_METHODS);
    config.setAllowedHeaders(DEFAULT_ALLOWED_HEADERS);
    config.setAllowCredentials(DEFAULT_ALLOW_CREDENTIALS);
    config.setMaxAge(DEFAULT_MAX_AGE);
    corsConfigurations.put("/**", config);
}else {
    // 单个mapping的处理
    for (Element mapping : mappings) {
        CorsConfiguration config = new CorsConfiguration();
        if (mapping.hasAttribute("allowed-origins")) {
            String[] allowedOrigins = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-origins"), ",");
            config.setAllowedOrigins(Arrays.asList(allowedOrigins));
        }
        // ...
    }

解析完成后,通过MvcNamespaceUtils.registerCorsConfiguratoions注册

这边走的是spring bean容器管理的统一流程,现在转化为BeanDefinition然后再实例化。

// MvcNamespaceUtils
    public static RuntimeBeanReference registerCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations, ParserContext parserContext, Object source) {
        if (!parserContext.getRegistry().containsBeanDefinition(CORS_CONFIGURATION_BEAN_NAME)) {
            RootBeanDefinition corsConfigurationsDef = new RootBeanDefinition(LinkedHashMap.class);
            corsConfigurationsDef.setSource(source);
            corsConfigurationsDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
            if (corsConfigurations != null) {
                corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations);
            }
            parserContext.getReaderContext().getRegistry().registerBeanDefinition(CORS_CONFIGURATION_BEAN_NAME, corsConfigurationsDef);
            parserContext.registerComponent(new BeanComponentDefinition(corsConfigurationsDef, CORS_CONFIGURATION_BEAN_NAME));
        }
        else if (corsConfigurations != null) {
            BeanDefinition corsConfigurationsDef = parserContext.getRegistry().getBeanDefinition(CORS_CONFIGURATION_BEAN_NAME);
            corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations);
        }
        return new RuntimeBeanReference(CORS_CONFIGURATION_BEAN_NAME);
    }

注解初始化

在RequestMappingHandlerMapping的initCorsConfiguration中扫描使用CrossOrigin注解的方法,并提取信息。

RequestMappingHandlerMapping

RequestMappingHandlerMapping_initCorsConfiguration

// RequestMappingHandlerMapping
    @Override
    protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
        HandlerMethod handlerMethod = createHandlerMethod(handler, method);
        CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), CrossOrigin.class);
        CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);

        if (typeAnnotation == null && methodAnnotation == null) {
            return null;
        }

        CorsConfiguration config = new CorsConfiguration();
        updateCorsConfig(config, typeAnnotation);
        updateCorsConfig(config, methodAnnotation);

        // ... 设置默认值
        return config;
    }   

跨域请求处理

HandlerMapping在正常处理完查找处理器后,在AbstractHandlerMapping.getHandler中校验是否是跨域请求,如果是分两种进行处理:

  • 如果是预请求,将处理器替换为内部类PreFlightHandler

  • 如果是正常请求,添加CorsInterceptor拦截器

拿到处理器后,通过请求头是否包含Origin判断是否跨域,如果是跨域,通过UrlBasedCorsConfigurationSource获取跨域配置信息,并委托getCorsHandlerExecutionChain处理

UrlBasedCorsConfigurationSource是CorsConfigurationSource的实现,从类名就可以猜出这边request与CorsConfiguration的映射是基于url的。getCorsConfiguration中提取request中的url后,逐一验证配置是否匹配url。

    // UrlBasedCorsConfigurationSource
    public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
        String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
        for(Map.Entry<String, CorsConfiguration> entry : this.corsConfigurations.entrySet()) {
            if (this.pathMatcher.match(entry.getKey(), lookupPath)) {
                return entry.getValue();
            }
        }
        return null;
    }

    // AbstractHandlerMapping
    public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        Object handler = getHandlerInternal(request);
        // ...

        HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
        if (CorsUtils.isCorsRequest(request)) {
            CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
            CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
            CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
            executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
        }
        return executionChain;
    }
    // HttpHeaders
    public static final String ORIGIN = "Origin";

    // CorsUtils
    public static boolean isCorsRequest(HttpServletRequest request) {
        return (request.getHeader(HttpHeaders.ORIGIN) != null);
    }

通过请求头的http方法是否options判断是否预请求,如果是使用PreFlightRequest替换处理器;如果是普通请求,添加一个拦截器CorsInterceptor。

PreFlightRequest是CorsProcessor对于HttpRequestHandler的一个适配器。这样HandlerAdapter直接使用HttpRequestHandlerAdapter处理。

CorsInterceptor 是CorsProcessor对于HnalderInterceptorAdapter的适配器。

    // AbstractHandlerMapping
    protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
            HandlerExecutionChain chain, CorsConfiguration config) {

        if (CorsUtils.isPreFlightRequest(request)) {
            HandlerInterceptor[] interceptors = chain.getInterceptors();
            chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
        }
        else {
            chain.addInterceptor(new CorsInterceptor(config));
        }
        return chain;
    }


    private class PreFlightHandler implements HttpRequestHandler {

        private final CorsConfiguration config;

        public PreFlightHandler(CorsConfiguration config) {
            this.config = config;
        }

        @Override
        public void handleRequest(HttpServletRequest request, HttpServletResponse response)
                throws IOException {

            corsProcessor.processRequest(this.config, request, response);
        }
    }


    private class CorsInterceptor extends HandlerInterceptorAdapter {

        private final CorsConfiguration config;

        public CorsInterceptor(CorsConfiguration config) {
            this.config = config;
        }

        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                Object handler) throws Exception {

            return corsProcessor.processRequest(this.config, request, response);
        }
    }

    // CorsUtils
    public static boolean isPreFlightRequest(HttpServletRequest request) {
        return (isCorsRequest(request) && request.getMethod().equals(HttpMethod.OPTIONS.name()) &&
                request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD) != null);
    }

可以去github查看: https://github.com/haplone/spring_doc/blob/master/mvc/cors.md

参考:
https://spring.io/blog/2015/06/08/cors-support-in-spring-framework

 

分享到:
评论

相关推荐

    详解Spring MVC CORS 跨域

    CORS 跨域是 Web 开发中一个非常重要的概念,Spring MVC 提供了多种方式来实现 CORS 跨域,包括使用 `@CrossOrigin` 注解和配置文件。通过使用 CORS 跨域,我们可以实现不同域名下的资源共享,提高 Web 应用程序的...

    Spring MVC 与 CORS跨域的详细介绍

    Spring MVC 与 CORS 跨域的详细介绍 本文对 Spring MVC 与 CORS 跨域的详细介绍,包括 CORS 的知识点和如何在 Spring MVC 中配置 CORS。 CORS 简介 同源策略(same origin policy)是浏览器安全的基石。在同源...

    基于Spring和Spring MVC实现可跨域访问的REST服务

    这篇博客“基于Spring和Spring MVC实现可跨域访问的REST服务”深入探讨了如何使用这两个流行的Java框架来创建这样的服务。Spring作为核心框架提供依赖注入和整体架构支持,而Spring MVC作为其Web模块,专门用于构建...

    spring4.3 实现跨域CORS的方法

    在 Spring 4.3 框架中,实现跨域 CORS 需要在 Controller 中添加 @CrossOrigin 注解,并在 Spring MVC 配置文件中添加 CORS 配置。 首先,在 Controller 中添加 @CrossOrigin 注解,以允许跨域请求。例如: ```java...

    CORS 跨域配置和jar文件

    总结,CORS跨域配置是解决前后端跨域问题的关键,而在Java环境中,我们可以通过编写Filter或使用Spring MVC的注解来实现。配合jar文件,可以将这些配置部署到服务器,为Web应用提供跨域支持。理解并正确运用CORS,能...

    cors-filter-1.7.jar spring解决跨域问题 java

    本篇将详细讲解如何利用Spring解决跨域问题,以及如何使用`cors-filter-1.7.jar`和`java-property-utils-1.9.1.jar`这两个库来辅助实现。 一、Spring解决跨域问题的基本原理 1. CORS定义:跨域是指浏览器遵循同源...

    spring MVC uploadify

    注意,为了实现跨域预览,可能需要在服务器端添加CORS配置。 总结起来,结合Spring MVC的`multipartResolver`和uploadify,我们可以实现一个高效、用户友好的图片上传和预览功能。这涉及到后端文件上传的处理、前端...

    springmvc跨域处理和过滤器方式跨域处理主要代码

    Spring MVC提供了多种解决跨域问题的方法,其中两种常见的策略是使用CORS(Cross-Origin Resource Sharing)配置和Filter过滤器。 ### CORS配置 CORS是一种标准的W3C规范,它允许服务器放宽同源策略的限制。在...

    详解SpringMVC解决跨域的两种方案

    SpringMVC 跨域解决方案 SpringMVC 跨域解决方案是指在 SpringMVC 框架中解决跨站 HTTP 请求(Cross-site HTTP request)的两种方案。跨域是指发起请求的资源所在域不同于请求指向资源所在域的 HTTP 请求。在前后端...

    在 Spring Web MVC 环境下使用 Dojo

    5. **数据绑定**:Dojo 控件如 DataGrid 可以通过 JSONP 或 CORS 从 Spring MVC 提供的 RESTful API 获取数据。确保服务器端支持跨域请求,或设置响应头以允许 JSONP 请求。 **四、DataGridTest 示例** 在你提供的...

    spring-mvc-showcase-case1-client

    2. 安全性:考虑跨域资源共享(CORS)策略,确保前后端通信的安全。 3. 数据交互:定义RESTful API接口,使用AngularJS的$http或$resource服务进行数据交换。 4. 路由管理:AngularJS的$routeProvider或ui-router...

    spring mvc 3.2 rest配置 文件

    7. **CORS支持** - 对于跨域资源共享(CORS),Spring MVC提供了`CorsRegistry`和`CorsConfiguration`来配置允许哪些来源进行访问。 8. **Validation** - 使用JSR-303/JSR-349提供的注解进行数据验证,例如`@Not...

    Spring MVC中自带的跨域问题解决方法

    本文将深入探讨Spring MVC如何解决跨域问题,并通过一个具体的例子来展示解决方案。 首先,理解什么是跨域。跨域是由于浏览器的同源策略(Same-origin policy)导致的一种安全机制。同源策略规定,JavaScript只能...

    过滤器或拦截器跨域CORS处理

    默认情况下,浏览器执行同源策略,阻止跨域请求,但CORS提供了安全地实现跨域数据交互的机制。 **过滤器(Filter)** `CrossDomainFilter.java` 是一个实现了Servlet Filter接口的类,用于处理HTTP请求和响应。在...

    Spring实现处理跨域请求代码详解

    Spring实现处理跨域请求代码详解 Spring实现处理跨域请求代码详解是指在Spring框架中如何处理跨域请求,以便让客户端可以跨域访问...我们可以使用CORS协议来解决跨域问题,并使用Spring框架来实现处理跨域请求的功能。

    java spring mvc网站报XMLHttpRequest cannot load,只能get访问不能post访问 的解决办法.zip

    解决方案是配置Spring MVC的CORS(Cross-Origin Resource Sharing)过滤器,允许特定的跨域请求。 2. **HTTP方法限制**:有些安全设置或浏览器插件可能会限制非GET请求。检查浏览器设置,确保POST请求未被禁用。 3...

    单点登陆实现(完全跨域、单点退出)

    在这个例子中,我们基于Spring MVC、Maven、WebService和Memcached来实现一个功能完善的单点登录系统,同时支持完全跨域和单点退出。 1. **Spring MVC**: Spring MVC是Spring框架的一个模块,主要用于构建Web应用...

    springboot 跨域请求

    Spring Boot提供了多种方式来实现跨域支持,其中最常用的是通过CORS(Cross-Origin Resource Sharing,跨源资源共享)机制。下面我们将详细探讨如何在Spring Boot中配置CORS,以及它与MyBatis框架的集成。 首先,...

Global site tag (gtag.js) - Google Analytics