spring MVC cors跨域实现源码解析

发布时间 - 2026-01-10 22:56:39    点击率:

名词解释:跨域资源共享(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的调用栈

通过代码可以看到这边解析

跨域信息的配置可以以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_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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持!


# spring  # cors  # 跨域  # mvc  # Spring+SpringMVC+JDBC实现登录的示例(附源码)  # Spring MVC 启动过程源码分析详解  # SpringMVC源码解析之消息转换器HttpMessageConverter  # Spring SpringMVC在启动完成后执行方法源码解析  # SpringMVC的源码解析  # 从源码角度看spring mvc的请求处理过程  # 配置文件  # 默认设置  # 默认值  # 的是  # 都是  # 判断是否  # 几个  # 拦截器  # 多个  # 两种  # 这是一个  # 要看  # 如果没有  # 可以看到  # 然后再  # 转化为  # 是一个非常  # 资源共享  # 启动时  # 猜出 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: ChatGPT回答中断怎么办 引导AI继续输出完整内容的方法  在Oracle关闭情况下如何修改spfile的参数  猎豹浏览器开发者工具怎么打开 猎豹浏览器F12调试工具使用【前端必备】  Win11怎么关闭资讯和兴趣_Windows11任务栏设置隐藏小组件  Linux系统命令中screen命令详解  黑客如何通过漏洞一步步攻陷网站服务器?  如何确保FTP站点访问权限与数据传输安全?  javascript事件捕获机制【深入分析IE和DOM中的事件模型】  如何在服务器上配置二级域名建站?  Linux网络带宽限制_tc配置实践解析【教程】  iOS中将个别页面强制横屏其他页面竖屏  制作无缝贴图网站有哪些,3dmax无缝贴图怎么调?  如何在香港服务器上快速搭建免备案网站?  ,怎么在广州志愿者网站注册?  佛山网站制作系统,佛山企业变更地址网上办理步骤?  网站制作软件免费下载安装,有哪些免费下载的软件网站?  Android中Textview和图片同行显示(文字超出用省略号,图片自动靠右边)  如何在Windows环境下新建FTP站点并设置权限?  PHP 500报错的快速解决方法  东莞市网站制作公司有哪些,东莞找工作用什么网站好?  如何在IIS管理器中快速创建并配置网站?  手机钓鱼网站怎么制作视频,怎样拦截钓鱼网站。怎么办?  Laravel如何实现多表关联模型定义_Laravel多对多关系及中间表数据存取【方法】  如何用花生壳三步快速搭建专属网站?  如何基于云服务器快速搭建个人网站?  长沙企业网站制作哪家好,长沙水业集团官方网站?  如何续费美橙建站之星域名及服务?  高端网站建设与定制开发一站式解决方案 中企动力  Laravel怎么在Blade中安全地输出原始HTML内容  零服务器AI建站解决方案:快速部署与云端平台低成本实践  ,在苏州找工作,上哪个网站比较好?  如何使用 jQuery 正确渲染 Instagram 风格的标签列表  惠州网站建设制作推广,惠州市华视达文化传媒有限公司怎么样?  php静态变量怎么调试_php静态变量作用域调试技巧【解答】  JavaScript中如何操作剪贴板_ClipboardAPI怎么用  Laravel如何创建自定义中间件?(Middleware代码示例)  香港服务器如何优化才能显著提升网站加载速度?  如何在Windows 2008云服务器安全搭建网站?  猪八戒网站制作视频,开发一个猪八戒网站,大约需要多少?或者自己请程序员,需要什么程序员,多少程序员能完成?  Laravel如何使用Telescope进行调试?(安装和使用教程)  在线制作视频的网站有哪些,电脑如何制作视频短片?  javascript如何操作浏览器历史记录_怎样实现无刷新导航  Laravel如何使用Service Provider服务提供者_Laravel依赖注入与容器绑定【深度】  Laravel如何设置定时任务(Cron Job)_Laravel调度器与任务计划配置  如何在腾讯云免费申请建站?  Laravel怎么实现前端Toast弹窗提示_Laravel Session闪存数据Flash传递给前端【方法】  为什么php本地部署后css不生效_静态资源加载失败修复技巧【技巧】  Laravel全局作用域是什么_Laravel Eloquent Global Scopes应用指南  EditPlus 正则表达式 实战(3)  标题:Vue + Vuex 项目中正确使用 JWT 进行身份认证的实践指南