Spring MVC 跨域(CORS)源码深度解析:从 @CrossOrigin 到 DefaultCorsProcessor 的完整链路

发布时间:2026/9/20 23:52:50
Spring MVC 跨域(CORS)源码深度解析:从 @CrossOrigin 到 DefaultCorsProcessor 的完整链路 Spring MVC 跨域CORS源码深度解析从 CrossOrigin 到 DefaultCorsProcessor 的完整链路【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter导读跨域资源共享CORS是 Web 开发中绕不开的经典问题。本文以 Spring MVC 的跨域实现为主线从CrossOrigin注解与mvc:corsXML 两种配置方式入手逐层剖析MappingRegistry#register、initCorsConfiguration、CorsBeanDefinitionParser、CorsInterceptor、DefaultCorsProcessor等核心类的实现完整还原一次跨域请求在 Spring MVC 中的处理链路。读完本文你将掌握 Spring MVC 跨域配置的两种姿势、CorsConfiguration各属性含义以及从请求进入到响应头写出的底层运行机制可直接用于日常跨域问题的排查与方案设计。一、Spring MVC 跨域的整体脉络在 Spring MVC 中跨域处理贯穿两个阶段配置阶段应用启动时无论是通过CrossOrigin注解还是mvc:corsXML 标签最终都会解析出一份CorsConfiguration对象并与具体的HandlerMethod或 URL 路径模式建立映射关系。请求阶段每次请求进入DispatcherServlet后HandlerMapping在组装HandlerExecutionChain时判断是否为跨域请求若是则向执行链中追加跨域拦截器CorsInterceptor或预检处理器PreFlightHandler最终由CorsProcessor完成校验与响应头写入。这个先配置、后拦截的结构与仓库中另一篇文档 Spring-MVC-HandlerMapping 中getHandler的执行链路紧密相关——跨域分支正是在getHandler组装执行链时插入的。二、注解方式CrossOrigin2.1 最小可运行的跨域 Demo在原文档中给出了一个典型的注解跨域示例这里完整保留并加以注释package com.huifer.source.controller; import org.springframework.web.bind.annotation.*; import java.util.HashMap; /** * 在类级别标注 CrossOrigin对该 Controller 下所有接口生效 * maxAge 3600 表示预检请求结果缓存 3600 秒 */ CrossOrigin(maxAge 3600) RequestMapping(/) RestController public class JSONController { ResponseBody GetMapping(value /json) public Object ob() { HashMapString, String hashMap new HashMap(); hashMap.put(1, a); return hashMap; } }CrossOrigin注解可标注在类级别对类内所有映射方法生效或方法级别仅对单个方法生效两者可以叠加方法级别配置会与类级别配置合并。注解支持的属性与CorsConfiguration字段一一对应origins允许的请求源、allowedHeaders允许的请求头、methods允许的 HTTP 方法、exposedHeaders暴露给浏览器的响应头、allowCredentials是否允许携带 Cookie、maxAge预检结果缓存时长。2.2 切入点MappingRegistry#register注解配置的解析发生在应用启动注册 HandlerMethod 时调用链为org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#registerHandlerMethodorg.springframework.web.servlet.handler.AbstractHandlerMethodMapping.MappingRegistry#registerMappingRegistry#register是整个注册流程的核心它做了四件事创建HandlerMethod、校验映射唯一性、建立 url 与 mapping 的索引、初始化并缓存跨域配置public void register(T mapping, Object handler, Method method) { // 上锁保证注册过程的线程安全 this.readWriteLock.writeLock().lock(); try { // 创建 HandlerMethod通过 handler 创建处理的对象(controller) HandlerMethod handlerMethod createHandlerMethod(handler, method); assertUniqueMethodMapping(handlerMethod, mapping); // 设置值mapping - handlerMethod this.mappingLookup.put(mapping, handlerMethod); // 获取 url ListString directUrls getDirectUrls(mapping); for (String url : directUrls) { // 建立 url - mapping 的索引 this.urlLookup.add(url, mapping); } String name null; if (getNamingStrategy() ! null) { name getNamingStrategy().getName(handlerMethod, mapping); addMappingName(name, handlerMethod); } /** * 跨域设置 * {link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#initCorsConfiguration(Object, Method, RequestMappingInfo)} **/ CorsConfiguration corsConfig initCorsConfiguration(handler, method, mapping); if (corsConfig ! null) { // 将 handlerMethod 与跨域配置建立映射 this.corsLookup.put(handlerMethod, corsConfig); } this.registry.put(mapping, new MappingRegistration(mapping, handlerMethod, directUrls, name)); } finally { // 开锁 this.readWriteLock.writeLock().unlock(); } }MappingRegistry内部维护了多个映射表其中与跨域直接相关的是corsLookupMapHandlerMethod, CorsConfigurationkey 是HandlerMethodvalue 是跨域配置。该内部类的完整结构可参见仓库文档 Spring-mvc-MappingRegistry。当应用关闭或映射被移除时unregister方法也会同步清理corsLookup中的对应条目保证配置与 handler 的生命周期一致。2.3 初始化跨域配置initCorsConfigurationregister方法中调用的initCorsConfiguration在RequestMappingHandlerMapping中实现它负责从类和方法上读取CrossOrigin注解并组装CorsConfigurationOverride protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { // 重新创建为什么不作为参数传递还有别的实现方法 HandlerMethod handlerMethod createHandlerMethod(handler, method); // 获取 bean 类型 Class? beanType handlerMethod.getBeanType(); // 获取注解信息支持 CrossOrigin 的合并属性语义如 AliasFor CrossOrigin typeAnnotation AnnotatedElementUtils.findMergedAnnotation(beanType, 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); // 如果允许的方法为空则回退到 RequestMappingInfo 中声明的方法 if (CollectionUtils.isEmpty(config.getAllowedMethods())) { for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) { config.addAllowedMethod(allowedMethod.name()); } } // 应用默认值并返回 return config.applyPermitDefaultValues(); }其中AnnotatedElementUtils.findMergedAnnotation负责从类上查找合并后的注解CrossOrigin本身带有AliasFor(annotation Origin.class, attribute value)之类的别名语义findMergedAnnotation可以正确解析这类合并属性。下图为调试过程中initCorsConfiguration的执行现场可以看到typeAnnotation、methodAnnotation的解析结果关键处理逻辑合并顺序先updateCorsConfig(config, typeAnnotation)合并类级别注解再updateCorsConfig(config, methodAnnotation)合并方法级别注解因此方法级别配置会覆盖/补充类级别配置方法回退若合并后allowedMethods仍为空则取mappingInfo.getMethodsCondition().getMethods()即GetMapping/RequestMapping中声明的 HTTP 方法作为允许方法这也是上面 Demo 中GetMapping最终解析出GET的原因默认值填充最后调用config.applyPermitDefaultValues()为未显式配置的项补充默认值如允许来源、允许方法、缓存时长等保证后续处理阶段拿到的配置总是完整可用的。2.4 配置合并细节updateCorsConfigupdateCorsConfig是initCorsConfiguration的私有辅助方法它对已有的CorsConfiguration做增量补充private void updateCorsConfig(CorsConfiguration config, Nullable CrossOrigin annotation) { if (annotation null) { return; } // 允许的来源 for (String origin : annotation.origins()) { config.addAllowedOrigin(resolveCorsAnnotationValue(origin)); } // 允许的 HTTP 方法 for (RequestMethod method : annotation.methods()) { config.addAllowedMethod(method.name()); } // 允许的请求头 for (String header : annotation.allowedHeaders()) { config.addAllowedHeader(resolveCorsAnnotationValue(header)); } // 暴露给前端 JS 的响应头 for (String header : annotation.exposedHeaders()) { config.addExposedHeader(resolveCorsAnnotationValue(header)); } // 是否允许携带凭证Cookie 等 String allowCredentials resolveCorsAnnotationValue(annotation.allowCredentials()); if (true.equalsIgnoreCase(allowCredentials)) { config.setAllowCredentials(true); } else if (false.equalsIgnoreCase(allowCredentials)) { config.setAllowCredentials(false); } else if (!allowCredentials.isEmpty()) { throw new IllegalStateException(CrossOrigins allowCredentials value must be \true\, \false\, or an empty string (\\): current value is [ allowCredentials ]); } // 预检结果缓存时长-1 表示不设置 if (annotation.maxAge() 0 config.getMaxAge() null) { config.setMaxAge(annotation.maxAge()); } }这里值得注意的细节resolveCorsAnnotationValue负责解析注解中的占位符如${my.origin}形式的环境变量引用使配置可以在运行时动态生效allowCredentials只接受true、false、三种取值其他值会直接抛出IllegalStateException从代码层面对配置合法性做了硬性校验当CrossOrigin使用默认值时注解属性多为空数组updateCorsConfig不会向配置中添加任何内容最终完全依赖applyPermitDefaultValues()填充默认值。2.5 解析结果缓存到 corsLookup完成上述组装后回到register方法若corsConfig ! null则将其放入corsLookupif (corsConfig ! null) { this.corsLookup.put(handlerMethod, corsConfig); }也就是说注解方式下跨域配置的注册动作在应用启动阶段就已完成后续请求处理时只需从corsLookup中按HandlerMethod取出即可无需重复解析。三、XML 配置方式mvc:cors除了注解Spring MVC 还支持在 XML 配置文件中声明式地配置跨域规则适合对全局路径模式做统一定义。3.1 配置示例mvc:cors mvc:mapping path/** allowed-originshttp://domain1.com, http://domain2.com allowed-methodsGET, PUT allowed-headersheader1, header2, header3 exposed-headersheader1, header2 allow-credentialsfalse max-age3600 / mvc:mapping path/** allowed-originshttp://domain1.com / /mvc:cors各属性含义与CorsConfiguration字段一一对应XML 属性对应字段说明path——URL 路径模式跨域规则匹配的路径如/**匹配全部allowed-originsallowedOrigins允许的请求来源逗号分隔多个allowed-methodsallowedMethods允许的 HTTP 方法逗号分隔allowed-headersallowedHeaders允许的请求头逗号分隔exposed-headersexposedHeaders暴露给浏览器 JS 的响应头allow-credentialsallowCredentials是否允许携带凭证Cookiemax-agemaxAge预检请求结果的缓存时长秒此外mvc:cors标签不带任何子标签时表示最简配置仅声明mvc:cors/mvc:cors就会生成一个针对/**路径、使用默认值的CorsConfiguration。3.2 标签解析入口MvcNamespaceHandlermvc命名空间下所有标签的解析入口是org.springframework.web.servlet.config.MvcNamespaceHandler它在init()中为每个子标签注册对应的BeanDefinitionParserpublic class MvcNamespaceHandler extends NamespaceHandlerSupport { Override public void init() { // 注解驱动 registerBeanDefinitionParser(annotation-driven, new AnnotationDrivenBeanDefinitionParser()); // 默认的 servlet 处理器 registerBeanDefinitionParser(default-servlet-handler, new DefaultServletHandlerBeanDefinitionParser()); // 拦截器 registerBeanDefinitionParser(interceptors, new InterceptorsBeanDefinitionParser()); // 资源 registerBeanDefinitionParser(resources, new ResourcesBeanDefinitionParser()); // 视图控制器 registerBeanDefinitionParser(view-controller, new ViewControllerBeanDefinitionParser()); // 重定向视图控制器 registerBeanDefinitionParser(redirect-view-controller, new ViewControllerBeanDefinitionParser()); registerBeanDefinitionParser(status-controller, new ViewControllerBeanDefinitionParser()); // 视图解析器 registerBeanDefinitionParser(view-resolvers, new ViewResolversBeanDefinitionParser()); // tiles / freemarker / groovy / script-template 配置器 registerBeanDefinitionParser(tiles-configurer, new TilesConfigurerBeanDefinitionParser()); registerBeanDefinitionParser(freemarker-configurer, new FreeMarkerConfigurerBeanDefinitionParser()); registerBeanDefinitionParser(groovy-configurer, new GroovyMarkupConfigurerBeanDefinitionParser()); registerBeanDefinitionParser(script-template-configurer, new ScriptTemplateConfigurerBeanDefinitionParser()); // 跨域处理 registerBeanDefinitionParser(cors, new CorsBeanDefinitionParser()); } }可见mvc:cors对应的解析器是CorsBeanDefinitionParser其继承结构如下3.3 核心解析逻辑CorsBeanDefinitionParser#parse实现BeanDefinitionParser接口的解析器都有一个parse方法CorsBeanDefinitionParser的parse目标很明确读取 XML 标签属性初始化CorsConfiguration最后注册到 Spring 容器public class CorsBeanDefinitionParser implements BeanDefinitionParser { Override Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { MapString, CorsConfiguration corsConfigurations new LinkedHashMap(); ListElement mappings DomUtils.getChildElementsByTagName(element, mapping); if (mappings.isEmpty()) { // 最简配置无任何 mapping 子标签时生成针对 /** 的默认配置 CorsConfiguration config new CorsConfiguration().applyPermitDefaultValues(); corsConfigurations.put(/**, config); } else { // 遍历每个 mvc: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)); } if (mapping.hasAttribute(allowed-methods)) { String[] allowedMethods StringUtils.tokenizeToStringArray(mapping.getAttribute(allowed-methods), ,); config.setAllowedMethods(Arrays.asList(allowedMethods)); } if (mapping.hasAttribute(allowed-headers)) { String[] allowedHeaders StringUtils.tokenizeToStringArray(mapping.getAttribute(allowed-headers), ,); config.setAllowedHeaders(Arrays.asList(allowedHeaders)); } if (mapping.hasAttribute(exposed-headers)) { String[] exposedHeaders StringUtils.tokenizeToStringArray(mapping.getAttribute(exposed-headers), ,); config.setExposedHeaders(Arrays.asList(exposedHeaders)); } if (mapping.hasAttribute(allow-credentials)) { config.setAllowCredentials(Boolean.parseBoolean(mapping.getAttribute(allow-credentials))); } if (mapping.hasAttribute(max-age)) { config.setMaxAge(Long.parseLong(mapping.getAttribute(max-age))); } // 以 path 属性为 key 存储并应用默认值 corsConfigurations.put(mapping.getAttribute(path), config.applyPermitDefaultValues()); } } // 注册到 Spring 容器 MvcNamespaceUtils.registerCorsConfigurations( corsConfigurations, parserContext, parserContext.extractSource(element)); return null; } }这段代码对应的行为可以归纳为逗号分割StringUtils.tokenizeToStringArray(..., ,)将逗号分隔的字符串拆分为数组因此 XML 中的allowed-originshttp://domain1.com, http://domain2.com会被解析为两个元素按 path 归组corsConfigurations是一个MapString, CorsConfigurationkey 是path属性值value 是解析出的配置。配置示例中的第一条规则最终会得到allowedOrigins[http://domain1.com, http://domain2.com]、allowedMethods[GET, PUT]、maxAge3600的结果支持多条规则多个mvc:mapping可以按不同 path 配置不同规则请求时会按路径匹配选择最合适的配置。3.4 注册到容器registerCorsConfigurations解析完成后MvcNamespaceUtils.registerCorsConfigurations将配置以名为mvcCorsConfigurations的LinkedHashMapBean 注册进容器public static RuntimeBeanReference registerCorsConfigurations( Nullable MapString, CorsConfiguration corsConfigurations, ParserContext context, Nullable Object source) { // 判断是否已存在跨域 beanbeanName: mvcCorsConfigurations if (!context.getRegistry().containsBeanDefinition(CORS_CONFIGURATION_BEAN_NAME)) { RootBeanDefinition corsDef new RootBeanDefinition(LinkedHashMap.class); corsDef.setSource(source); corsDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); if (corsConfigurations ! null) { corsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations); } context.getReaderContext().getRegistry().registerBeanDefinition(CORS_CONFIGURATION_BEAN_NAME, corsDef); // 注册组件并通知监听器 context.registerComponent(new BeanComponentDefinition(corsDef, CORS_CONFIGURATION_BEAN_NAME)); } else if (corsConfigurations ! null) { // 已存在则补充构造参数 BeanDefinition corsDef context.getRegistry().getBeanDefinition(CORS_CONFIGURATION_BEAN_NAME); corsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations); } return new RuntimeBeanReference(CORS_CONFIGURATION_BEAN_NAME); }这里有几个细节值得展开Bean 名CORS_CONFIGURATION_BEAN_NAME即mvcCorsConfigurations该 Bean 是一个LinkedHashMap构造参数就是 path 到CorsConfiguration的映射角色标记BeanDefinition.ROLE_INFRASTRUCTURE标记该 Bean 为基础设施 Bean不会被普通的组件扫描等逻辑误处理幂等合并若同一配置中多次声明mvc:cors或与annotation-driven等标签共用第二次不会再新建 Bean而是向已有 Bean 的构造参数中追加新的配置映射实现规则合并。容器启动后该Map会被注入到RequestMappingHandlerMapping的corsConfigurationSource中作为全局跨域配置供请求阶段使用——这正是注解配置与 XML 配置在请求阶段汇合的桥梁。四、跨域信息载体CorsConfiguration无论是注解方式还是 XML 方式解析结果最终都沉淀为org.springframework.web.cors.CorsConfiguration。它的核心字段与语义如下/** * 允许请求源 */ Nullable private ListString allowedOrigins; /** * 允许的 HTTP 方法 */ Nullable private ListString allowedMethods; /** * 解析后的 HTTP 方法集合默认值 DEFAULT_METHODS */ Nullable private ListHttpMethod resolvedMethods DEFAULT_METHODS; /** * 允许的请求头 */ Nullable private ListString allowedHeaders; /** * 暴露给前端的响应头 */ Nullable private ListString exposedHeaders; /** * 是否允许携带 cookies 等凭证 */ Nullable private Boolean allowCredentials; /** * 预检结果缓存有效期秒 */ Nullable private Long maxAge;字段与 CORS 响应头的对应关系字段对应响应头作用allowedOriginsAccess-Control-Allow-Origin告诉浏览器允许哪些源跨域访问allowedMethodsAccess-Control-Allow-Methods预检响应中声明允许的 HTTP 方法allowedHeadersAccess-Control-Allow-Headers预检响应中声明允许的请求头exposedHeadersAccess-Control-Expose-Headers允许前端 JS 读取的响应头白名单allowCredentialsAccess-Control-Allow-Credentials是否允许携带 Cookie、Authorization 等凭证maxAgeAccess-Control-Max-Age浏览器缓存预检结果的时长减少预检请求次数另外CorsConfiguration还提供了combine(CorsConfiguration other)方法用于配置合并请求阶段会用到以及applyPermitDefaultValues()用于补齐未配置项的默认值。可以推断allowedOrigins、allowedHeaders等字段在未配置与显式配置为*之间是有语义区分的这也是框架将允许所有表达为通配符而非空列表的原因。五、请求处理阶段跨域如何生效配置在启动阶段准备完毕后真正拦截并处理跨域请求发生在每次请求的 HandlerMapping 解析环节。5.1 getHandler 中的跨域分支AbstractHandlerMapping#getHandler在执行链组装完成后追加了跨域处理逻辑Override Nullable public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { Object handler getHandlerInternal(request); if (handler null) { handler getDefaultHandler(); } if (handler null) { return null; } // Bean name or resolved handler? if (handler instanceof String) { String handlerName (String) handler; handler obtainApplicationContext().getBean(handlerName); } HandlerExecutionChain executionChain getHandlerExecutionChain(handler, request); if (logger.isTraceEnabled()) { logger.trace(Mapped to handler); } else if (logger.isDebugEnabled() !request.getDispatcherType().equals(DispatcherType.ASYNC)) { logger.debug(Mapped to executionChain.getHandler()); } // 判断是否为跨域请求 if (CorsUtils.isCorsRequest(request)) { // 全局配置来自 mvc:cors 等 XML 配置 CorsConfiguration globalConfig this.corsConfigurationSource.getCorsConfiguration(request); // 当前 handler 的跨域配置来自 CrossOrigin 注解即 corsLookup CorsConfiguration handlerConfig getCorsConfiguration(handler, request); // 两者合并handler 级配置优先 CorsConfiguration config (globalConfig ! null ? globalConfig.combine(handlerConfig) : handlerConfig); // 组装跨域执行链 executionChain getCorsHandlerExecutionChain(request, executionChain, config); } return executionChain; }5.2 如何判断是跨域请求CorsUtils.isCorsRequest判断依据非常朴素——请求头中是否携带Originpublic static boolean isCorsRequest(HttpServletRequest request) { // 是否携带请求头 Origin return (request.getHeader(HttpHeaders.ORIGIN) ! null); }在调试时模拟请求GET http://localhost:9999/json Origin: localhostrequest.getHeader(origin)返回localhost请求因此被判定为跨域请求进入跨域处理分支下图展示了这一调试现场5.3 配置合并全局配置与 handler 配置跨域处理分支中出现了两种配置来源this.corsConfigurationSource.getCorsConfiguration(request)全局配置即 XMLmvc:cors解析后注册的mvcCorsConfigurations映射按请求 URL 路径匹配getCorsConfiguration(handler, request)handler 级配置来自注解方式存入corsLookup的CorsConfiguration。两者通过combine方法合并globalConfig.combine(handlerConfig)合并规则可以概括为handler 级配置注解优先未被 handler 配置覆盖的项沿用全局配置。因此实际项目中可以全局兜底 注解细调组合使用。5.4 跨域拦截器的创建getCorsHandlerExecutionChain合并出最终配置后getCorsHandlerExecutionChain根据请求类型决定执行链的组装方式protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, Nullable CorsConfiguration config) { if (CorsUtils.isPreFlightRequest(request)) { // 预检请求使用 PreFlightHandler 替换原 handler并保留原拦截器 HandlerInterceptor[] interceptors chain.getInterceptors(); chain new HandlerExecutionChain(new PreFlightHandler(config), interceptors); } else { // 普通跨域请求向执行链末尾追加 CorsInterceptor chain.addInterceptor(new CorsInterceptor(config)); } return chain; }这里区分了两类请求预检请求Preflight浏览器在实际跨域请求前对非简单请求如携带自定义头、非 GET/POST/HEAD 方法会先发送一个OPTIONS预检请求询问服务器是否允许。此时 Spring MVC 用PreFlightHandler作为处理器直接响应不再执行真实的业务 Controller实际请求普通跨域请求简单请求或已通过预检的请求则追加CorsInterceptor到执行链中。5.5 跨域拦截器CorsInterceptorCorsInterceptor是AbstractHandlerMapping的内部类它同时实现了HandlerInterceptorAdapter与CorsConfigurationSource将持有的配置暴露给处理器/** * 跨域拦截器 */ private class CorsInterceptor extends HandlerInterceptorAdapter implements CorsConfigurationSource { Nullable private final CorsConfiguration config; public CorsInterceptor(Nullable CorsConfiguration config) { this.config config; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 委托给 CorsProcessor 处理 return corsProcessor.processRequest(this.config, request, response); } Override Nullable public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { return this.config; } }拦截器在preHandle阶段将配置、请求、响应一并交给corsProcessor默认实现为DefaultCorsProcessor处理跨域逻辑因此从拦截器中剥离出来保持了单一职责。5.6 核心处理器DefaultCorsProcessor#processRequestDefaultCorsProcessor是CorsProcessor接口的默认实现负责实际的校验与响应头写入Override SuppressWarnings(resource) public boolean processRequest(Nullable CorsConfiguration config, HttpServletRequest request, HttpServletResponse response) throws IOException { // 判断是否跨域请求 if (!CorsUtils.isCorsRequest(request)) { return true; } ServletServerHttpResponse serverResponse new ServletServerHttpResponse(response); // 判断响应中是否已包含 Access-Control-Allow-Origin if (responseHasCors(serverResponse)) { logger.trace(Skip: response already contains \Access-Control-Allow-Origin\); return true; } ServletServerHttpRequest serverRequest new ServletServerHttpRequest(request); // 同源请求直接跳过 if (WebUtils.isSameOrigin(serverRequest)) { logger.trace(Skip: request is from same origin); return true; } boolean preFlightRequest CorsUtils.isPreFlightRequest(request); if (config null) { // 没有跨域配置预检请求直接拒绝普通请求放行由浏览器自行拦截 if (preFlightRequest) { rejectRequest(serverResponse); return false; } else { return true; } } // 核心校验与响应头写入 return handleInternal(serverRequest, serverResponse, config, preFlightRequest); }processRequest的处理顺序非常清晰是一个典型的前置检查 核心处理结构非跨域请求请求头无Origin直接放行响应已带 CORS 头说明后续过滤器/处理器已写入过Access-Control-Allow-Origin跳过避免重复处理同源请求WebUtils.isSameOrigin判断请求来源与目标同源直接放行无配置场景区分预检与普通请求——预检请求因无法确认服务器策略而直接拒绝返回 403普通请求则放行浏览器会依据响应头自行决定是否拦截核心处理handleInternal中完成Origin匹配、allowedMethods/allowedHeaders校验并向响应写入Access-Control-Allow-*系列响应头。从源码结构看handleInternal是校验与响应头写入的落点它校验请求的Origin是否在allowedOrigins中、预检请求的方法与头是否被允许然后据此写出Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers、Access-Control-Max-Age、Access-Control-Allow-Credentials等响应头校验失败如 Origin 不在白名单时则拒绝请求。六、两种配置方式对比与实战建议维度CrossOrigin注解mvc:corsXML配置粒度类/方法级别精确到单个接口路径模式级别可全局统配生效时机启动注册HandlerMethod时解析存入corsLookup启动解析 XML 时生成注册为mvcCorsConfigurationsBean配置来源AnnotatedElementUtils.findMergedAnnotationCorsBeanDefinitionParser#parse组合方式类级别 方法级别合并多条mvc:mapping按 path 归组请求阶段二者最终都汇入getHandler的跨域分支通过combine合并后交给CorsProcessor同左实战建议接口级精准控制用CrossOrigin全局统一策略如网关统一放行白名单用mvc:cors生产环境尽量避免使用allowed-origins*与allow-credentialstrue同时开启CorsConfiguration的校验逻辑会对其做出限制携带凭证时不能使用通配符来源若发现跨域配置不生效可优先检查Origin头是否携带、响应中是否已被其他过滤器写入 CORS 头、全局配置与注解配置的combine结果是否符合预期浏览器层面的拦截其实发生在响应返回之后——服务器侧即使放行浏览器仍会依据响应头决定是否将数据暴露给 JS这也是为什么会出现接口能通、前端拿不到数据的现象。七、总结本文沿着配置阶段 → 请求阶段的完整链路结合仓库源码还原了 Spring MVC 跨域处理的实现配置阶段CrossOrigin注解在MappingRegistry#register中经initCorsConfiguration、updateCorsConfig组装成CorsConfiguration存入corsLookupmvc:cors标签则由CorsBeanDefinitionParser#parse解析并经registerCorsConfigurations注册为全局配置 Bean请求阶段AbstractHandlerMapping#getHandler通过CorsUtils.isCorsRequest识别跨域请求合并全局与 handler 级配置后为预检请求装配PreFlightHandler、为普通请求装配CorsInterceptor最终统一由DefaultCorsProcessor#processRequest完成校验与响应头写入。Spring MVC 的跨域设计本质上是声明式配置 拦截器链 策略处理器的组合配置与处理解耦、全局与局部可合并、预检与实际请求分流。理解了这条链路无论是排查线上跨域问题还是为其他框架设计类似能力都能做到心中有数。延伸阅读Spring-MVC-HandlerMappinggetHandler的完整执行链路与 URL 匹配细节Spring-mvc-MappingRegistryMappingRegistry内部各映射表的注册、校验与移除逻辑。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/doocs/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考