Spring Boot 如何处理跨域请求(CORS):深度调研报告

Spring Boot 如何处理跨域请求(CORS):深度调研报告

执行摘要

跨源资源共享(CORS,Cross-Origin Resource Sharing)是现代 Web 开发中不可避免的基础课题。当浏览器执行的前端应用与后端 API 不处于同一源(协议+域名+端口)时,浏览器的同源策略会默认拦截这些跨域请求,而 CORS 协议提供了一套基于 HTTP 头的受控放行机制,使服务器能够精确声明哪些外部源可以访问其资源。Spring Framework 早在 4.2 版本就将 CORS 作为"一等公民"纳入 Spring MVC 的核心处理链,通过 HandlerMapping 内置机制统一处理预检请求与实际请求,开发者无需手写 Filter 即可声明式地完成配置。

Spring Boot 3.x 继承了 Spring Framework 6.x 的 CORS 体系,为开发者提供了由细到粗的四种主要配置方式:方法/类级 @CrossOrigin 注解、全局 WebMvcConfigurer.addCorsMappings() 路径映射、独立 CorsFilter Bean 注册,以及在引入 Spring Security 时的 CorsConfigurationSource + .cors() 集成配置。这四种方式共享同一套底层校验内核 DefaultCorsProcessor,通过 checkOrigin → checkMethods → checkHeaders 三段式校验确保跨域请求符合 W3C/WHATWG Fetch 规范要求。调研发现,所有方式的配置合并遵循"加法式"规则——allowedOriginsallowedMethods 取并集,但 allowCredentialsmaxAge 等单值属性由局部配置覆盖全局配置。

本次调研的核心发现之一,是 allowedOrigins="*"allowCredentials=true 的组合冲突。这一限制源自 W3C CORS 规范中"凭证模式下 Access-Control-Allow-Origin 不得使用通配符"的硬性要求,自 Spring Framework 5.3(Spring Boot 2.4.0)起在框架层面以 IllegalArgumentException 形式强制执行。在所有活跃的 Spring Boot 版本中,开发者必须改用 Spring 5.3 引入的 allowedOriginPatterns 来实现"允许所有源 + 携带凭证"的场景,该属性采用 AntPathMatcher 模式匹配而非精确字符串比对,能够动态回显请求方的 Origin。

从安全维度审视,CORS 配置的不当是一个被严重低估的风险。奇安信与清华大学的联合测量研究显示,全球约 27.5% 的 CORS 配置网站存在不安全配置,其中最危险的是将 Access-Control-Allow-Origin 反射为请求方的 Origin 同时开启凭证传递——这一配置实质上完全绕过了同源策略。更值得警惕的是,11 款主流 CORS 框架中有 8 款在遇到 Origin:* + Credentials:true 这种矛盾配置时会自动转为反射 Origin(CVE-2018-8014),这构成了一个隐蔽但严重的安全漏洞。

从工程实践维度来看,Spring Security 集成是 CORS 踩坑的最高频场景。当 SecurityFilterChain 未显式调用 .cors() 时,OPTIONS 预检请求不携带 JSESSIONID,会被认证过滤器判定为未认证而返回 401/403,导致浏览器阻断实际请求。此外,现代浏览器 Chrome 80+ 的 SameSite=Lax 默认 Cookie 策略构成了跨域凭证传递的第二道防线,即使后端正确配置了 CORS,前端 Cookie 也必须显式设置 SameSite=None; Secure 才能在跨域请求中传递。在微服务架构中,API Gateway 层与下游服务的双重 CORS 配置会产生重复响应头,直接导致浏览器拒绝请求,这是分布式系统中最常见的 CORS 运维故障。

本报告从 CORS 协议原理、Spring 源码实现、四种配置方式详解、Spring Security 集成、Cookie 与凭证传递、安全最佳实践、常见错误与排查、2.x→3.x 迁移变更、性能优化等九个维度,对 Spring Boot CORS 处理机制进行了全面深度剖析,并在每个关键点提供了可运行的代码示例和生产环境配置建议。


第一章 引言

1.1 研究背景

在现代 Web 开发实践中,前后端分离架构已成为主流范式。前端应用通常部署在独立的静态资源服务器(或 CDN)上,与后端 API 服务运行在不同的源(origin)中。这里的"源"由协议(scheme)、域名(host)和端口(port)三要素共同定义——只要任一要素不同,浏览器即视为跨源。例如,前端 https://app.example.com 调用后端 https://api.example.com,虽然域名主体相同,但因子域不同而构成跨源;再如 http://localhost:3000 调用 http://localhost:8080,因端口不同同样构成跨源。

浏览器的同源策略(Same-Origin Policy)是 Web 安全的基石之一,它限制脚本(如 JavaScript 的 fetch()XMLHttpRequest)只能向加载当前页面的同一源发起请求,以防止恶意网站通过用户浏览器窃取其在其他站点的敏感数据。然而,合法的跨源需求(如前后端分离、微服务 API 网关、第三方 API 集成)必须被满足,CORS 协议正是为此而生——它允许服务器通过特定的 HTTP 响应头声明"我允许来自 XX 源的请求",浏览器在验证这些头后放行对应的跨域请求。

Spring Boot 作为 Java 生态中最流行的 Web 框架,对 CORS 提供了全方位的内置支持。但正是这种"全方位"带来了选择困难:开发者面对 @CrossOriginWebMvcConfigurerCorsFilter、Spring Security CorsConfigurationSource 等多种方式,往往不清楚该选择哪种、它们之间有何差异、以及如何避免常见的配置冲突。更复杂的是,Spring Boot 3.x 基于 Spring Framework 6.x 和 Jakarta EE 9+,与广泛使用的 Spring Boot 2.x 存在若干破坏性变更,CORS 配置在迁移过程中可能产生隐蔽的兼容性问题。

1.2 研究范围与方法

本调研聚焦于 Spring Boot 3.x 的 CORS 处理机制,同时系统对比 Spring Boot 2.x 的差异以确保迁移项目的兼容性。调研采用三阶段顺序执行的多 Agent 方法论:第一阶段(Agent 1)执行综合 Web 调研,从官方文档、权威技术博客和社区实践中收集 CORS 协议原理、配置方式和版本差异的基础信息;第二阶段(Agent 2)在第一阶段发现的基础上进行源码级深度分析,验证配置方式背后的底层实现机制,并深入探讨 Spring Security 集成、Cookie 凭证传递和性能优化等高级主题;第三阶段(Agent 3)交叉验证前两阶段的所有发现,识别共识与矛盾,补充安全最佳实践和常见错误排查。

调研数据来源涵盖 15+ 篇高质量文献,包括 Mozilla MDN 的 CORS 权威参考(A 级)、W3C/WHATWG Fetch 规范(A 级)、Spring Framework 和 Spring Security 官方文档(A 级)、Baeldung 等知名技术博客(B 级)、Spring 源码仓库与社区源码分析(B 级),以及奇安信与清华大学的 CORS 安全测量研究(A 级)。所有关键声明均经过多源交叉验证,对存在矛盾或条件性的结论已明确标注。

1.3 报告结构

本报告共分九章。第二章从协议层面讲解 CORS 的工作原理,包括同源策略、简单请求与预检请求的判定、关键 HTTP 头和预检缓存。第三章深入 Spring Framework 的源码实现,追踪从请求到达到响应返回的完整 CORS 处理链。第四章详解 Spring Boot 3.x 的四种配置方式,每种方式均配有完整代码示例和适用场景说明。第五章专门讨论 Spring Security 集成中的 CORS 配置,这是生产环境踩坑最频繁的场景。第六章探讨 Cookie 与凭证传递的深度话题,包括 SameSite 策略和 JSESSIONID 跨域。第七章从安全视角审视 CORS 配置的风险与防护。第八章汇总常见错误与排查方法。第九章聚焦 Spring Boot 2.x → 3.x 的 CORS 迁移变更。第十章讨论性能优化策略。最后以综合分析和参考文献收尾。


第二章 CORS 协议原理

2.1 同源策略与 CORS 的关系

同源策略是浏览器最核心的安全机制之一。根据 Mozilla MDN 的定义,同源策略限制了一个源中的脚本与另一个源中资源之间的交互,这种限制涵盖了 DOM 访问、Cookie/Storage 读取和跨域网络请求三个层面(Mozilla, 2025, "Cross-Origin Resource Sharing (CORS) — HTTP | MDN", https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)。同源策略的存在使得恶意网站无法通过 JavaScript 读取用户在银行网站上的数据——即使恶意网站成功诱导用户浏览器发起了指向银行 API 的请求,浏览器的同源策略也会阻止恶意网站的脚本读取该响应。

然而,同源策略过于严格,无法满足合法的跨源业务需求。CORS 协议的引入正是为了在同源策略的框架下提供一种受控的"豁免"机制。服务器通过在响应中添加特定的 HTTP 头(如 Access-Control-Allow-Origin),明确告知浏览器"我允许来自某源的跨域请求访问我的资源"。浏览器在验证这些头信息后,才会将响应数据交给发起请求的脚本。这一机制的关键在于:CORS 的拦截和放行完全由浏览器端执行,服务器只是"声明"策略,实际"执法"者是浏览器。这意味着如果用非浏览器客户端(如 curl、Postman)发起跨域请求,CORS 机制不会生效——服务器本身不会因为 CORS 而拒绝任何请求。

CORS 规范最初由 W3C 制定,现已被 WHATWG 的 Fetch 规范吸收和维护(WHATWG, 2024, "Fetch Standard — CORS Protocol", https://fetch.spec.whatwg.org/)。Fetch 规范中对 CORS 协议的描述更为精确,引入了"credentials mode"(凭证模式)和"response tainting"(响应污染级别)等概念,这些概念在后文的凭证传递分析中至关重要。

2.2 简单请求与预检请求

CORS 协议将跨域请求分为两类:简单请求(Simple Request)和预检请求(Preflight Request)。区分二者的意义在于:简单请求只需一次客户端-服务器往返即可完成,而预检请求需要先发一个 OPTIONS 探测请求,服务器确认后才能发送实际请求,总共两次往返。

根据 MDN 的规范说明,一个请求被判定为"简单请求"需同时满足以下所有条件:仅使用 GETHEADPOST 方法;除浏览器自动设置的头外,手动设置的请求头只能属于 CORS 安全列表头集合(AcceptAccept-LanguageContent-LanguageContent-Type);且 Content-Type 仅限于 application/x-www-form-urlencodedmultipart/form-datatext/plain 三种。当请求不满足上述任一条件——例如使用了 PUTDELETEPATCH 方法,或 Content-Typeapplication/json(这是现代 RESTful API 最常用的格式),或携带了 AuthorizationX-Requested-With 等自定义请求头——浏览器就会将其归类为"需要预检的请求"(Mozilla, 2025, MDN CORS)。

由于现代 Web API 几乎都以 JSON 作为数据格式并使用 Authorization 头传递 JWT Token,绝大多数实际的跨域 API 请求都会触发预检流程。这意味着预检请求的优化(后文详述)对 API 性能有实际影响。

2.3 预检请求的工作流程

预检请求是一个以 OPTIONS 方法发送的 HTTP 请求,其目的是在发送实际请求之前,先"询问"服务器是否允许即将发起的跨域请求。预检请求的流程如下:

浏览器在发送实际请求前,先构造一个 OPTIONS 请求,其中携带两个关键请求头:Access-Control-Request-Method(告知服务器实际请求将使用的方法,如 PUT)和 Access-Control-Request-Headers(告知服务器实际请求将携带的自定义头,如 authorization, content-type)。服务器收到预检请求后,检查自身的 CORS 策略,如果允许,则在响应中返回一组 Access-Control-Allow-* 头予以确认,包括 Access-Control-Allow-Origin(允许的源)、Access-Control-Allow-Methods(允许的方法)、Access-Control-Allow-Headers(允许的头)、Access-Control-Max-Age(预检结果可缓存的秒数)。浏览器收到预检响应后,验证这些确认头是否覆盖了实际请求的需求。如果通过,浏览器才会发送实际请求;如果不通过,浏览器会直接报错,实际请求不会被发出。

实际请求发出后,服务器在响应中再次携带 Access-Control-Allow-Origin 等头(但不需要再携带 Allow-MethodsAllow-Headers,因为这些已在预检阶段确认)。浏览器对实际请求的响应进行二次校验,通过后才将响应数据交给 JavaScript 脚本。

2.4 关键 HTTP 头详解

CORS 协议涉及一组以 Access-Control-* 为前缀的 HTTP 头,分为请求头(由浏览器发送)和响应头(由服务器发送)两类。

请求头(浏览器→服务器):

  • Origin:标识跨域请求的来源源(格式为 scheme://host:port),浏览器在所有跨域请求中自动携带,开发者无法手动设置或修改。
  • Access-Control-Request-Method:仅在预检请求中出现,告知实际请求将使用的方法。
  • Access-Control-Request-Headers:仅在预检请求中出现,告知实际请求将携带的自定义头列表(逗号分隔)。

响应头(服务器→浏览器):

  • Access-Control-Allow-Origin:指定允许访问资源的源。可以是具体的源(如 https://app.example.com),也可以是 *(表示允许所有源)。但当 Access-Control-Allow-Credentials: true 时,此头不能为 *,必须精确回显请求方的 Origin——这是 CORS 安全模型的核心约束之一。
  • Access-Control-Allow-Methods:仅在预检响应中出现,指定允许的 HTTP 方法列表。
  • Access-Control-Allow-Headers:仅在预检响应中出现,指定实际请求中允许携带的自定义头。
  • Access-Control-Expose-Headers:指定哪些响应头可以被前端 JavaScript 读取。默认情况下,JavaScript 只能读取 CORS 安全列表中的头(Cache-ControlContent-LanguageContent-LengthContent-TypeExpiresLast-ModifiedPragma)。如果前端需要读取自定义响应头(如 X-Total-CountX-Request-Id),必须在此头中列出。
  • Access-Control-Allow-Credentials:指示是否允许浏览器在跨域请求中携带凭证(Cookie、HTTP 认证信息、客户端 SSL 证书)。当为 true 时,Access-Control-Allow-Origin 不能为 *,且浏览器端 fetch() 必须设置 credentials: 'include'
  • Access-Control-Max-Age:仅在预检响应中出现,指定预检结果可被浏览器缓存的秒数。在缓存有效期内,对同一来源的相同跨域请求,浏览器不会再次发送预检请求。该值受浏览器自身上限约束

——Chrome 上限为 7200 秒(2 小时),Firefox 上限为 86400 秒(24 小时)——即使服务器返回更大的值,浏览器也按自身上限截断(Mozilla, 2025, MDN CORS)。

2.5 Vary 头与缓存正确性

一个容易被忽略但至关重要的细节是 Vary 响应头在 CORS 中的作用。当服务器根据请求的 Origin 头动态返回不同的 Access-Control-Allow-Origin 值时,如果缺少 Vary: Origin 头,浏览器或中间 CDN 可能缓存一个针对源 A 的响应并错误地返回给源 B 的请求,导致 CORS 校验失败或安全漏洞。Spring 的 DefaultCorsProcessor 在处理跨域请求时会自动添加 Vary: Origin 头(以及 Vary: Access-Control-Request-MethodVary: Access-Control-Request-Headers),以确保缓存正确性。这一行为在 Spring Framework 源码的 DefaultCorsProcessor 类中可以清晰看到,是 Spring CORS 实现的一个内置且关键的细节(出门向左, 2017, "spring MVC cors跨域实现源码解析", https://www.cnblogs.com/leftthen/p/6378090.html)。


第三章 Spring Framework 源码级 CORS 处理流程

3.1 处理入口:AbstractHandlerMapping

Spring MVC 对 CORS 的处理深度嵌入在请求分发链中,而非外挂式的 Filter。理解这一点的关键是追踪 AbstractHandlerMapping 的源码。根据源码级分析(出门向左, 2017, "spring MVC cors跨域实现源码解析", https://www.cnblogs.com/leftthen/p/6378090.html),`AbstractHandlerMapping.getHandler(HttpServletRequest request)` 方法是整个 CORS 处理的入口,其执行链路可精确追溯如下:

首先,getHandlerInternal(request) 获取目标 Handler(即 Controller 方法)。然后,系统构建 HandlerExecutionChain,将其与拦截器链组装。关键步骤在于 CORS 检测:通过 CorsUtils.isCorsRequest(request) 判断请求头中是否包含 Origin 字段——如果存在且该 Origin 与当前服务器不同源,则识别为跨域请求。一旦确认为跨域请求,系统会从两个来源获取 CORS 配置并合并:全局配置来自 UrlBasedCorsConfigurationSource.getCorsConfiguration(request)(对应 WebMvcConfigurer.addCorsMappings() 注册的路径规则),局部配置来自 getCorsConfiguration(handler, request)(即解析 @CrossOrigin 注解的结果)。两者通过 CorsConfiguration.combine() 方法完成合并。

合并完成后,系统根据请求是否为预检请求(检测方法是 CorsUtils.isPreFlightRequest(request),即 OPTIONS 方法且包含 Access-Control-Request-Method 头)做出分流处理。如果是预检请求,Handler 会被替换为内部类 PreFlightHandler,它直接返回 CORS 响应头而不执行实际的 Controller 逻辑;如果是普通的跨域请求(非预检),则在执行链中追加 CorsInterceptor 拦截器,该拦截器在 Controller 方法执行前后添加 CORS 校验和响应头注入。这一设计确保了预检请求不会走入业务逻辑,而实际请求的 CORS 头注入则与业务处理同步进行。

3.2 配置合并逻辑:CorsConfiguration.combine()

全局配置与局部(@CrossOrigin)配置的合并遵循精确定义的规则,由 CorsConfiguration#combine(CorsConfiguration) 方法实现。源码分析显示(出门向左, 2017),合并策略按属性类型区分:对于 allowedOriginsallowedMethodsallowedHeadersexposedHeaders 等集合类属性,合并后取两个配置的并集,即全局允许的源和局部允许的源都被合并为最终允许列表;对于 allowCredentialsmaxAge 等只能接受单一值的属性,局部配置覆盖全局配置——如果在 @CrossOrigin 上显式设置了 allowCredentials,则以注解值为准,否则继承全局配置。

这一"加法式"合并规则有一个重要的实际影响:如果你在全局配置中设置了 allowCredentials(true) 并允许了 https://domain1.com,而某个 Controller 方法上的 @CrossOrigin 额外允许了 https://domain2.com,那么对该 Controller 方法的请求最终的允许源列表是两个域的并集(domain1.comdomain2.com 都被允许),但凭证传递仍为 true。这种合并方式给开发者带来了灵活性,但也要求对全局和局部配置的交互保持清醒认知,避免因意外合并导致 CORS 策略宽于预期。

3.3 校验内核:DefaultCorsProcessor 三段式校验

无论是 Servlet Filter 层的 CorsFilter 还是 MVC 拦截器层的 CorsInterceptor,它们共享同一个校验内核——DefaultCorsProcessor。该处理器在 handleInternal() 方法中执行严格的三段式校验(CSDN/ximeneschen, 2022, "@CrossOrigin及其实现跨域原理", https://blog.csdn.net/cristianoxm/article/details/124840435):

第一段是 checkOrigin():检查请求的 Origin 是否在允许列表(allowedOrigins)或匹配允许的模式列表(allowedOriginPatterns)。如果允许列表为空或 Origin 不匹配,返回 null;如果匹配成功,返回将写入 Access-Control-Allow-Origin 响应头的值。这里的关键逻辑是:当 allowCredentialstrue 时,allowedOrigins 不能包含 "*"——如果检测到这种组合,Spring 5.3+ 会直接抛出 IllegalArgumentException,而非静默失败。这是本文反复强调的那个限制的源码落点。对于 allowedOriginPatternscheckOrigin() 使用 AntPathMatcher 进行模式匹配(如 "https://*.example.com" 可匹配 https://app.example.com),匹配成功后精确回显请求 Origin 而非通配符。

第二段是 checkMethods()(仅在预检请求中执行):检查 Access-Control-Request-Method 指定的方法是否在 allowedMethods 列表中。如果允许列表为 * 或包含该具体方法,返回该方法名(或 *),写入 Access-Control-Allow-Methods 响应头;否则返回 null

第三段是 checkHeaders()(仅在预检请求中执行):检查 Access-Control-Request-Headers 中列出的每个头是否在 allowedHeaders 列表中。同样支持 * 通配符。匹配成功后返回这些头的列表,写入 Access-Control-Allow-Headers 响应头;任一头不匹配则返回 null

任何一段返回 null 都会导致请求被 rejectRequest() 拒绝。但一个微妙的细节是,这里的"拒绝"并非返回 HTTP 403 状态码——rejectRequest() 实际上返回一个不带任何 CORS 响应头的 200(或与方法对应的)响应,让浏览器基于"缺少 CORS 头"自行拦截请求。这一设计避免了服务器暴露 CORS 策略细节给未授权来源,但也有开发者误以为是服务器逻辑错误而难以排查。

3.4 CorsFilter vs CorsInterceptor 的执行时机

Spring 同时提供了 CorsFilter(Servlet Filter 层)和 CorsInterceptor(MVC 拦截器层)两条 CORS 处理路径,二者共享 DefaultCorsProcessor 但处于请求处理的不同阶段(CSDN/好运仔dzl, 2025, "SpringBoot源码解析(二十三):跨域处理CorsFilter的自动注册原理", https://blog.csdn.net/qq_50954361/article/details/148469418)。

CorsFilter 实现了 jakarta.servlet.Filter(Spring Boot 3.x)或 javax.servlet.Filter(2.x),它在 DispatcherServlet 之前执行。这意味着 CORS 处理发生在 Spring MVC 的请求分发之前——对于预检请求,可以直接返回 CORS 响应而不进入 MVC 链。CorsFilter 通常用于两种场景:一是需要以独立 Bean 形式管理 CORS 配置;二是与 Spring Security 集成时,需要确保 CORS 在安全过滤器链之前或紧随其后处理,以避免预检请求被认证逻辑拦截(详见第五章)。

CorsInterceptor 实现了 HandlerInterceptor,它在 DispatcherServlet 分发请求到 Handler 之前执行。这是 Spring MVC @CrossOriginWebMvcConfigurer.addCorsMappings() 配置的默认处理路径。CorsInterceptor 的优势是能够访问 Handler 级别的 @CrossOrigin 注解信息,实现细粒度的配置合并;其局限是无法在 Spring Security 等更早的 Filter 层介入。

理解二者差异的实际意义在于:当项目中同时存在 CorsFilter(或 Spring Security 的 CORS 配置)和 @CrossOrigin/WebMvcConfigurer 配置时,两者可能产生交互。一般而言,CorsFilter 先执行,如果它已经完成了 CORS 校验并注入了响应头,后续的 CorsInterceptor 不会重复处理;如果 CorsFilter 判定请求非跨域(如没有 Origin 头),CorsInterceptor 则正常接管。在配置时应避免冲突,通常推荐只选择一种路径以保证行为一致。

3.5 Spring Boot 的 CORS 自动配置

Spring Boot 还提供了 CorsAutoConfiguration 自动配置类,当 classpath 上存在相关类且开发者未手动注册 CorsFilter 时,Spring Boot 会自动注册一个基于 UrlBasedCorsConfigurationSourceCorsFilter Bean。源码分析显示(CSDN/好运仔dzl, 2025),自动配置的触发条件包括 classpath 上存在 CorsFilterCorsConfigurationSource 等类,以及开发者未显式提供这些 Bean。自动注册的 CorsFilter 通过 FilterRegistrationBean 指定其在过滤器链中的顺序,默认优先级较高。

然而,这一自动配置在引入 Spring Security 时有重要的交互效应:如果 Spring Security 的 SecurityFilterChain 未调用 .cors(),自动注册的 CorsFilter 可能因执行顺序问题而被 Security 的认证过滤器"抢先"拦截预检请求。这进一步印证了第五章将要强调的核心建议——使用 Spring Security 时必须显式配置 .cors()


第四章 Spring Boot 3.x 的 CORS 配置方式详解

Spring Boot 3.x 继承了 Spring Framework 6.x 的 CORS 体系,提供了从细粒度到全局的多种配置方式。本章逐一详解每种方式的用法、适用场景、优缺点,并给出完整可运行的代码示例。

4.1 方式一:@CrossOrigin 注解(方法级/类级)

@CrossOrigin 是最细粒度的 CORS 配置方式,可直接标注在 Controller 方法或类上。根据 Spring Framework 官方文档(Spring, 2024, "CORS — Spring Framework Reference", https://docs.spring.io/spring-framework/reference/web/webmvc-cors.html),`@CrossOrigin` 的默认行为是:允许所有源(origins = "*")、所有头、该方法映射的所有 HTTP 方法;maxAge 默认 30 分钟(1800 秒);allowCredentials 默认不启用(false)。

方法级使用示例:

java
复制代码
@RestController @RequestMapping("/api") public class MyController { @CrossOrigin(origins = "https://frontend.example.com") @GetMapping("/data/{id}") public ResponseEntity<?> getData(@PathVariable Long id) { return ResponseEntity.ok(service.getData(id)); } @CrossOrigin(origins = {"https://app1.example.com", "https://app2.example.com"}, allowedHeaders = {"Authorization", "Content-Type"}, methods = {RequestMethod.GET, RequestMethod.POST}, maxAge = 3600) @PostMapping("/submit") public ResponseEntity<?> submit(@RequestBody RequestDto dto) { return ResponseEntity.ok(service.submit(dto)); } }

类级使用示例,类级配置会被所有方法继承:

java
复制代码
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600) @RestController @RequestMapping("/account") public class AccountController { @GetMapping("/{id}") public Account retrieve(@PathVariable Long id) { // 该方法继承类级 @CrossOrigin 配置 return accountService.retrieve(id); } @DeleteMapping("/{id}") public void delete(@PathVariable Long id) { // 也在类级 CORS 策略覆盖范围内 accountService.delete(id); } }

类级与方法级可组合使用,Spring 会按 combine() 规则合并属性。一个重要的细节是:方法级 @CrossOrigin 默认允许的 HTTP 方法仅是该处理器方法映射的那一个方法——例如 @GetMapping 标注的方法,其默认 methodsGET,而非全部 HTTP 方法。这与部分社区博客"允许所有方法"的笼统表述不同,是一个容易混淆的点。

@CrossOrigin 的优势在于精确控制——只有标注的方法/类受 CORS 策略约束,未标注的方法不受影响。其局限是当需要统一的跨域策略时,逐一标注较为繁琐,且配置分散难以维护。适用场景:少量特定接口需要与默认全局策略不同的 CORS 配置,如某个开放 API 允许任意源访问而其他接口仅限内部域名。

4.2 方式二:WebMvcConfigurer.addCorsMappings() 全局配置

当需要对特定 URL 路径模式统一配置 CORS 时,WebMvcConfigurer.addCorsMappings() 是首选方案。通过实现 WebMvcConfigurer 接口并重写 addCorsMappings 方法,可按路径模式注册 CORS 规则:

java
复制代码
@Configuration public class WebCorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { // 全局 API 路径 registry.addMapping("/api/**") .allowedOrigins("https://domain1.com", "https://domain2.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH") .allowedHeaders("Authorization", "Content-Type", "X-Requested-With") .exposedHeaders("X-Total-Count", "X-Request-Id") .allowCredentials(true) .maxAge(3600); // 公开接口,允许任意源 registry.addMapping("/public/**") .allowedOrigins("*") .allowedMethods("GET"); // 需要凭证 + 动态源,使用 allowedOriginPatterns registry.addMapping("/auth/**") .allowedOriginPatterns("https://*.example.com") .allowedMethods("POST", "GET") .allowCredentials(true) .maxAge(1800); } }

全局配置默认行为是允许所有源、所有头,但方法仅限 GETHEADPOST。这与方法级 @CrossOrigin 默认仅允许映射的单个方法不同。使用时需注意:若不显式设置 allowedMethods,某些 PUT/DELETE 请求会被默认配置拦截。

WebMvcConfigurer 方式的优势是路径级统一控制,配置集中、易于维护,且支持路径通配符。其局限是配置粒度为 URL 模式,无法针对单个 Controller 方法定制(但可与 @CrossOrigin 组合使用,二者按 combine() 规则合并)。适用场景:项目中大部分接口遵循统一的 CORS 策略,仅个别接口需要例外。这是生产环境中最常用的配置方式。

4.3 方式三:CorsFilter Bean 注册

当需要以 Servlet Filter 形式处理 CORS(例如需要在过滤器链更早阶段介入,或与 Spring Security 等框架更紧密集成时),可注册一个 CorsFilter Bean,传入一个 CorsConfigurationSource

java
复制代码
@Configuration public class CorsFilterConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("https://domain1.com"); // Spring Boot 2.4+ / 3.x 推荐用 OriginPatterns config.addAllowedOriginPattern("https://*.example.com"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.addExposedHeader("X-Total-Count"); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

CorsFilter 的优势在于它位于 Servlet 过滤器链中,在 DispatcherServlet 之前执行,因此能更早地完成 CORS 处理。这对于 Spring Security 集成尤其重要——在 Security 的过滤链中嵌入 CorsFilter(通过 CorsConfigurationSource Bean)可以确保预检请求不会被认证过滤器拦截。CorsFilter 也适合需要精确控制过滤器执行顺序的场景。

CorsFilter 的局限在于它不直接感知 @CrossOrigin 注解(因为注解在 MVC 层解析),因此如果项目中同时使用了 @CrossOriginCorsFilter 与 MVC 层的 CorsInterceptor 可能各自独立校验,需注意配置一致性。适用场景:需要与 Spring Security 集成、或项目未使用 Spring MVC 的全部栈(如仅使用 Spring WebFlux 的 Servlet 模式)时。

4.4 方式四:Spring Security 中的 CorsConfigurationSource + .cors()

当项目引入 Spring Security 时,CORS 配置必须与 Security 过滤链正确集成。Spring Security 官方文档明确指出(Spring, 2024, "CORS — Spring Security Reference", https://docs.spring.io/spring-security/reference/servlet/integrations/cors.html),最简洁的方式是提供一个 CorsConfigurationSource Bean 并在 SecurityFilterChain 中调用 .cors()

java
复制代码
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(Customizer.withDefaults()) // 启用 CORS,使用下方的 CorsConfigurationSource Bean .csrf(csrf -> csrf.disable()) // 跨域场景通常禁用 CSRF(或按需配置) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) // ... 其他安全配置 ; return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("https://app.example.com")); configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); configuration.setAllowedHeaders(List.of("Authorization", "Content-Type")); configuration.setExposedHeaders(List.of("X-Request-Id")); configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }

一个关键的便利机制是:如果 classpath 上存在 Spring MVC 且调用了 .cors(Customizer.withDefaults())未提供 CorsConfigurationSource Bean,Spring Security 会自动复用 Spring MVC 的 CORS 配置(即 WebMvcConfigurer.addCorsMappings() 注册的规则)。这意味着在纯 MVC + Security 的项目中,你可以只在 WebMvcConfigurer 中配置 CORS,然后在 Security 中调用 .cors() 即可,无需重复配置。但建议在生产环境中显式提供 CorsConfigurationSource Bean,以避免隐式依赖带来的混淆。

Spring Boot 3.x 搭配的 Spring Security 6.x 废弃了 Spring Security 5.x 的链式 DSL 写法 http.cors().and().csrf().disable()...,改为 Lambda 风格 http.cors(Customizer.withDefaults()) 或 Kotlin DSL。这是从 Spring Boot 2.x 迁移到 3.x 时最常见的代码改动之一。Spring Security 7.x 还引入了 PreFlightRequestHandlerPreFlightRequestFilter,用于在 CorsFilter 之外处理预检请求,支持以每个 SecurityFilterChain 为单位配置不同的 CorsConfigurationSource,这为多租户或 API 分层场景提供了更灵活的控制(Spring, 2024, Spring Security Reference)。

4.5 四种方式对比与选型建议

维度@CrossOriginWebMvcConfigurerCorsFilterSpring Security .cors()
配置粒度方法/类级URL 路径模式URL 路径模式URL 路径模式
作用层级MVC 拦截器层MVC 拦截器层Servlet Filter 层Security Filter 层(最先执行)
执行时机DispatcherServlet 分发后DispatcherServlet 分发后DispatcherServlet 之前Security 过滤链入口
感知 @CrossOrigin合并取决于配置源
与 Spring Security 协同弱(需额外 .cors()弱(需额外 .cors()中等强(原生集成)
适用场景个别接口定制路径级统一策略需要 Filter 层控制安全项目必选

选型建议:

  • 纯 Spring MVC 项目(无 Spring Security):首选 WebMvcConfigurer.addCorsMappings(),配合 @CrossOrigin 处理个别例外接口。
  • Spring Security 项目:必须使用 Spring Security 的 .cors() + CorsConfigurationSource,可同时用 @CrossOrigin 做局部微调。
  • 需要在 Filter 层统一控制(如自定义认证 Filter 之前处理 CORS):使用 CorsFilter Bean。
  • 微服务/API Gateway 架构:在 Gateway 层统一处理,下游服务不重复配置(避免双重头问题,详见第六章)。

第五章 Spring Security 集成中的 CORS 深度剖析

5.1 为什么 Spring Security 环境下 CORS 必须特殊处理

Spring Security 集成是 CORS 踩坑的最高频场景。问题的根源在于 FilterChainProxy 管理的 SecurityFilterChain 中,安全过滤器的执行顺序在 Spring MVC 的 CORS 处理之前(百度开发者中心, 2024, "SpringBoot中使用SpringSecurity时CorsFilter配置不生效的原因与解决方法", https://developer.baidu.com/article/detail.html?id=2767051)。

当未调用 .cors() 时,OPTIONS 预检请求的处理路径如下:浏览器发送 OPTIONS 请求,该请求不携带 Cookie(这是 CORS 规范的规定——预检请求永远不携带凭证,即使实际请求会携带),因此没有 JSESSIONID。请求首先进入 SecurityFilterChain,被 UsernamePasswordAuthenticationFilterBearerTokenAuthenticationFilter 等认证过滤器拦截。由于没有 Session/Token,Security 判定用户未认证,返回 401 Unauthorized 或 403 Forbidden。浏览器收到不含正确 CORS 头的错误响应,判定预检失败,阻断实际请求。

调用 http.cors(Customizer.withDefaults()) 后,Spring Security 会在 SecurityFilterChain最前端插入一个 CorsFilter,其位置在所有认证和授权过滤器之前。这样 OPTIONS 请求在进入认证逻辑之前,就已经被 CorsFilter 处理并返回了正确的 CORS 响应头,浏览器验证通过后发送实际请求,实际请求携带凭证进入正常的认证流程。

5.2 CORS 过滤器在 SecurityFilterChain 中的精确位置

深入 SecurityFilterChain 的过滤器链,CorsFilter 的位置并非随意。Spring Security 内部维护了一个有序的过滤器列表,CorsFilter(通过 .cors() 启用)被插入到 SecurityContextHolderFilter(或旧版的 SecurityContextPersistenceFilter)之后、UsernamePasswordAuthenticationFilter 等认证过滤器之前。这一位置选择有其深意:CORS 处理不需要 SecurityContext(预检请求本就无认证信息),但必须在任何认证尝试之前完成,以确保预检请求不会被认证逻辑阻断。

这种精确位置也意味着:如果你的 Security 配置中有自定义过滤器需要访问 CORS 响应头或需要在 CORS 之后、认证之前执行,可以通过 addFilterAfter()addFilterBefore() 精确控制位置。但一般而言,直接使用 .cors() 的默认位置即可满足绝大多数场景。

5.3 无状态(Stateless)会话策略下的 CORS

在 RESTful API 中常见的无状态会话策略(SessionCreationPolicy.STATELESS)下,CORS 的配置有一些特殊考量。无状态意味着服务器不创建或使用 HTTP Session,每次请求都必须携带认证凭证(如 JWT Token)。在这种策略下,认证不依赖 JSESSIONID,而是依赖 Authorization 头中的 Bearer Token。

对于预检请求,Authorization 头不会出现在预检请求中(预检请求只包含 Access-Control-Request-MethodAccess-Control-Request-Headers)。因此预检请求的拦截问题与有状态场景一致——都需要 .cors() 在认证之前处理。对于实际请求,Authorization 头由浏览器在跨域请求中携带(前提是前端 fetch 设置了 credentials: 'include'xhr.withCredentials = true,且 CORS 配置了 allowCredentials(true)allowedHeaders 包含 Authorization)。

值得注意的一个细节是:JWT Token 跨域传递比 Cookie 跨域传递阻力更小。因为 Token 放在 Authorization 头中而非 Cookie 头中,不受 SameSite Cookie 策略的影响——只要 CORS 配置正确允许 Authorization 头且 allowCredentials(true),Token 就能正常传递。这也是为什么前后端分离架构在 JWT 认证模式下更常见的 CORS 友好性原因。

5.4 Spring WebFlux 环境下的 CORS

上述讨论基于 Servlet 栈(Spring WebMVC)。对于 Spring WebFlux(响应式栈),CORS 的处理方式有细节差异。WebFlux 不使用 DispatcherServletHandlerInterceptor,而是使用 WebFilterHandlerFilterFunction。Spring WebFlux 提供了 CorsWebFilter(对应 Servlet 栈的 CorsFilter),其配置方式类似:

java
复制代码
@Configuration @EnableWebFluxSecurity public class WebFluxSecurityConfig { @Bean public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) { return http .cors(cors -> cors.configurationSource(corsConfigurationSource())) // ... 其他配置 .build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); // ... 同 Servlet 栈配置 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }

WebFlux 中的 @CrossOrigin 同样可用,Spring WebFlux 的 WebHandler 层会解析注解并应用 CORS 配置。在 Spring Cloud Gateway(基于 WebFlux)中,CORS 通常通过 CorsWebFilter 或 Gateway 的 cors 配置项处理,详见第十章的微服务架构讨论。


第六章 Cookie 与凭证传递深度

6.1 allowCredentials=true 的浏览器行为

当服务器端配置 allowCredentials(true) 且响应头为 Access-Control-Allow-Credentials: true 时,浏览器在跨域请求中的行为有以下关键变化:实际请求(非预检)会携带 Cookie(包括 JSESSIONID、自定义 Cookie)和 HTTP 认证信息(如 Basic Auth 头);浏览器允许 JavaScript 读取响应内容。但有一个重要的规范规定——预检请求永远不携带 Cookie,即使 allowCredentials=true。这是 CORS 规范的硬性规定,预检请求的目的是探测服务器策略,不应携带任何用户身份信息(Mozilla, 2025, MDN CORS)。

前端配合方面,如果使用 fetch() API,需要显式设置 credentials: 'include'

javascript
复制代码
fetch('https://api.example.com/data', { credentials: 'include', // 跨域携带 Cookie headers: { 'Content-Type': 'application/json' } });

如果使用 XMLHttpRequest,需要设置 xhr.withCredentials = true

javascript
复制代码
const xhr = new XMLHttpRequest(); xhr.withCredentials = true; xhr.open('GET', 'https://api.example.com/data'); xhr.send();

6.2 Access-Control-Allow-Origin 精确匹配的安全原理

在凭证模式下,Access-Control-Allow-Origin 不能为 *,必须精确回显请求方的 Origin。这一限制的深层安全原理在于防止"凭证泄露攻击"。

如果 Access-Control-Allow-Origin*,浏览器无法将响应关联到特定的请求来源,意味着任何恶意网站都可以通过用户的浏览器携带 Cookie 访问受保护资源,而浏览器无法区分合法和非法来源。精确匹配 Origin 确保了响应只对发起该跨域请求的页面可见,形成了"请求-响应"的强绑定关系。Spring 的 checkOrigin() 方法在 allowCredentials=true 时强制执行这一约束——这也是 allowedOrigins="*" + allowCredentials=true 抛出 IllegalArgumentException 的根本原因。

6.3 SameSite Cookie 属性:跨域凭证的第二道防线

现代浏览器 Chrome 80+ 默认 SameSite=Lax 的 Cookie 策略构成了跨域凭证传递的第二道防线(CSDN/Eward-an, 2026, "前端跨域进阶:CORS实战避坑", https://blog.csdn.net/an524415864/article/details/161681018)。即使后端正确设置了 Access-Control-Allow-Credentials: true,如果 Cookie 缺少 SameSite=None; Secure 属性,浏览器仍然不会在跨域请求中携带该 Cookie。

SameSite 属性有三个值:Strict(完全禁止跨站携带,即使导航链接也不携带)、Lax(默认值,允许顶级导航的 GET 请求携带,但禁止跨域 XHR/fetch 携带)、None(允许跨站携带,但必须同时设置 Secure)。对于需要在跨域 AJAX 请求中传递的 Cookie(如 JSESSIONID),必须设置为 SameSite=None; Secure

这意味着现代跨域认证需要"双层配置":CORS 层面的凭证允许(allowCredentials=true + 精确 Access-Control-Allow-Origin)+ Cookie 层面的跨站放行(SameSite=None; Secure),两者缺一不可。对于 JSESSIONID 的跨域传递场景,这要求 Servlet 容器的 Session Cookie 配置也必须相应调整。在 Spring Boot 中可通过以下方式配置:

java
复制代码
@Bean public WebServerFactoryCustomizer<TomcatServletWebServerFactory> cookieCustomizer() { return factory -> factory.addContextCustomizers(context -> { context.setSessionCookieDomain(".example.com"); // Spring Boot 3.x 中配置 SameSite // 注意:Servlet 6.0 规范尚未直接支持 SameSite,需通过 Tomcat 的 CookieProcessor }); }

或在响应拦截器中手动设置:

java
复制代码
@Component public class SameSiteCookieFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { filterChain.doFilter(request, response); Collection<String> headers = response.getHeaders(HttpHeaders.SET_COOKIE); if (!headers.isEmpty()) { response.setHeader(HttpHeaders.SET_COOKIE, headers.stream().map(h -> h + "; SameSite=None; Secure") .collect(Collectors.joining(","))); } } }

6.4 OAuth2 / JWT Token 跨域传递

对于使用 OAuth2 或 JWT Token 进行认证的系统,Token 通常通过 Authorization: Bearer <token> 头传递,而非 Cookie。这种模式在跨域场景下比 Cookie 模式更为简洁——Token 不受 SameSite 限制,只要 CORS 配置中 allowedHeaders 包含 AuthorizationallowCredentials=true(或对于纯 Token 无 Cookie 场景甚至可以用 allowCredentials=false + allowedOrigins="*"),Token 即可正常传递。

但需要注意:如果 Token 是通过 HttpOnly Cookie 自动携带的(如某些 OAuth2 实现),则回到 Cookie 跨域问题,需要 SameSite=None; Secure 配置。建议跨域认证场景优先考虑 Bearer Token + Authorization 头模式,以规避 Cookie 跨域的复杂性。


第七章 安全视角:CORS 配置风险与防护

7.1 CORS 配置不当导致的安全漏洞

CORS 配置的不当是一个被严重低估的安全风险。奇安信与清华大学的联合测量研究(A级学术研究)显示,全球约 27.5% 的 CORS 配置网站存在不安全配置,研究识别出 7 类典型误配置模式。其中最危险的配置是:将 Access-Control-Allow-Origin 反射为请求方的 Origin 同时开启凭证传递。这种配置实质上完全绕过了同源策略——任何恶意网站都可以通过用户浏览器携带其 Cookie 访问该 API,读取敏感数据。

更值得警惕的是,调研发现 11 款主流 CORS 框架中有 8 款在遇到 Origin:* + Credentials:true 这种矛盾配置时,会自动转为反射 Origin(CVE-2018-8014),这构成了一个隐蔽但严重的安全漏洞。虽然 Spring Framework 的处理方式是直接抛出 IllegalArgumentException 阻止此类配置,但开发者如果使用自定义 Filter 绕过 Spring 的 CORS 机制,仍可能无意中实现反射 Origin 的危险行为。

7.2 CORS 配置安全审计要点

基于安全研究结论,CORS 配置审计应关注以下要点:

审计点一:Origin 反射检测。 检查响应头 Access-Control-Allow-Origin 是否始终等于请求 Origin 头的值。如果是,且 Access-Control-Allow-Credentials: true,则存在严重的安全漏洞。合法的配置应该维护一个允许源的白名单,而非无条件反射。

审计点二:白名单过宽。 检查 allowedOrigins 是否包含不必要的通配符模式(如 https://*.com),这实际上允许了几乎所有 HTTPS 网站。应仅允许业务必需的具体域名。

审计点三:凭证模式下的头暴露。allowCredentials=true 时,检查 exposedHeaders 是否暴露了敏感的内部头(如 X-Debug-InfoServer 版本信息)。

审计点四:预检缓存的 maxAge 过长。 过长的 maxAge 意味着 CORS 策略变更后需要更长时间才能在客户端生效。生产环境建议 3600 秒(1 小时)到 86400 秒(24 小时)之间,平衡性能和安全策略生效速度。

审计点五:null Origin 处理。 某些场景(如 file:// 协议、sandbox iframe)下 Origin 头为 null。如果 CORS 配置允许 null Origin,可能被利用从本地文件执行跨域读取。应在白名单中拒绝 null

7.3 CSRF 与 CORS 的关系和区别

CSRF(Cross-Site Request Forgery)和 CORS 经常被混淆,但它们是不同的安全机制。CSRF 攻击是利用用户已登录的身份,诱导用户浏览器在不知情的情况下发送请求(如通过隐藏表单自动提交到银行转账 API)。CSRF 防护通常通过 CSRF Token(同步令牌模式)或 SameSite Cookie 实现。

CORS 与 CSRF 的关系在于:CORS 是浏览器对读取跨域响应的限制,而 CSRF 防护是对发送跨域请求的限制。关键点是——即使没有 CORS 配置(服务器不返回 CORS 头),浏览器仍然可以发送跨域请求(如 form 提交、img 标签),只是 JavaScript 无法读取响应。这意味着 CSRF 攻击不依赖 CORS 配置漏洞,CORS 不是 CSRF 的防护机制。

在 Spring Security 中,跨域场景通常需要禁用 CSRF(http.csrf(csrf -> csrf.disable()))或配置 CSRF 的 CORS 兼容性,因为 CSRF Token 默认通过 Cookie 或 Header 传递,跨域时可能产生冲突。正确做法是:跨域 API 认证依赖 Bearer Token(不受 CSRF 影响),并禁用基于 Session 的 CSRF 防护。


第八章 常见错误与排查指南

8.1 "No 'Access-Control-Allow-Origin' header is present"

这是最常见的 CORS 错误,浏览器错误信息通常为:"Access to fetch at '...' from origin '...' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource."

可能原因与排查:

  1. CORS 配置未生效。 最常见的原因。排查步骤:检查是否在 Spring Security 中调用了 .cors();检查 WebMvcConfigurer.addCorsMappings() 的路径模式是否匹配请求路径;检查 @CrossOrigin 是否加在了正确的 Controller/方法上。验证方法:在 Controller 方法中打日志确认请求是否到达——如果请求未到达 Controller,说明在 Filter 链或 Security 链中被拦截。
  2. Origin 不在允许列表。 检查请求的 Origin 头是否确实在 allowedOrigins 或匹配 allowedOriginPatterns。注意子域差异:https://app.example.comhttps://example.com 是不同的源。验证方法:在 CorsConfigurationcheckOrigin 逻辑处打断点或添加日志。
  3. 异常导致 CORS 头未注入。 如果 Controller 抛出未处理异常,Spring 的异常处理流程可能不会注入 CORS 响应头,导致浏览器看到"无 CORS 头"错误。排查方法:检查服务器日志是否有异常;在 @ControllerAdvice 全局异常处理器中确保异常响应也携带 CORS 头(或确保 CorsFilter 在异常处理之前已注入头)。
  4. 路径模式不匹配。 addCorsMappings("/api/**") 不匹配 /api-v2/data(虽然 /api-v2 不匹配 /api/**)。仔细检查 Ant 风格路径模式的匹配范围。

8.2 "The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*'"

完整错误:"The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'."

根因: 前端使用了 credentials: 'include',但服务器返回了 Access-Control-Allow-Origin: *。这在凭证模式下被禁止。

解决方案:

  • 后端从 allowedOrigins("*") 改为 allowedOriginPatterns("*")(Spring Boot 2.4+/3.x),框架会自动精确回显 Origin 而非 *
  • 或显式列出允许的源:allowedOrigins("https://app.example.com", "https://admin.example.com")
  • 前端如果不需要携带凭证,改为 credentials: 'omit'fetch 默认值),则服务器可以继续用 *

8.3 "The 'Access-Control-Allow-Origin' header contains multiple values"

完整错误:"The 'Access-Control-Allow-Origin' header contains multiple values 'https://a.com, https://b.com', but only one is allowed."

根因: 服务器(或反向代理链)返回了重复的 Access-Control-Allow-Origin 头。CORS 规范要求该头只能出现一次。

常见场景:

  • Nginx 层和 Spring Boot 应用层都配置了 CORS,各自添加了一个 Access-Control-Allow-Origin 头。
  • Spring Cloud Gateway 和下游服务同时配置 CORS。
  • 过滤器链中多个 Filter 各自添加 CORS 头。

解决方案: 确保整个请求链路上只有一个组件处理 CORS。推荐在 API Gateway/Nginx 层统一处理,并在下游服务中移除 CORS 配置。如果必须多层配置,使用 Nginx 的 proxy_hide_header 指令隐藏下游返回的 CORS 头,由 Nginx 层重新添加。

8.4 预检请求失败(OPTIONS 返回 401/403)

根因: 最常见于 Spring Security 环境,OPTIONS 预检请求被认证过滤器拦截。

排查与解决:

  • 确认 SecurityFilterChain 中调用了 .cors(Customizer.withDefaults())
  • 确认提供了 CorsConfigurationSource Bean(或 Spring MVC 的 CORS 配置存在,Security 会复用)。
  • 检查是否有自定义 Filter 在 CorsFilter 之前拦截了 OPTIONS 请求——通过 addFilterBefore 确保自定义 Filter 在 CORS 之后。

8.5 WebSocket 跨域与 CORS 的关系

一个容易被忽略的事实是:WebSocket 跨域独立于 HTTP CORS 机制(WHATWG, 2024, "Fetch Standard — CORS Protocol", https://fetch.spec.whatwg.org/)。WebSocket 协议在握手阶段使用 HTTP Upgrade 请求,但浏览器对 WebSocket 连接不应用同源策略——即 WebSocket 连接不受 CORS 限制。前端 JavaScript 可以向任意源的 WebSocket 端点发起连接,无需服务器配置 CORS。

然而,WebSocket 服务器仍应验证连接的 Origin 头以防止跨站 WebSocket 劫持攻击(CSWSH)。Spring 的 WebSocket 支持提供了 setAllowedOrigins() 配置来限制允许的源,这是应用层安全措施,非浏览器 CORS 机制。

8.6 文件上传跨域的特殊处理

文件上传使用 multipart/form-data Content-Type,理论上是 CORS 安全列表内的 Content-Type,不应触发预检请求。但实践中常见意外触发预检的情况——原因是部分前端框架(如 jQuery、某些 axios 配置)会在 Content-Type 后追加 ; charset=UTF-8,使其变为 multipart/form-data; charset=UTF-8,这超出了 CORS 安全列表,触发预检请求(CSDN/Eward-an, 2026, "前端跨域进阶:CORS实战避坑", https://blog.csdn.net/an524415864/article/details/161681018)。

解决方案: 前端避免为 multipart/form-data 设置 charset 后缀;后端在 allowedHeaders 中包含 Content-Type;或使用 allowedHeaders("*") 简化配置。


第九章 Spring Boot 2.x → 3.x 的 CORS 迁移变更

9.1 Jakarta EE 迁移的影响

Spring Boot 3.0 基于 Spring Framework 6.x,要求 Java 17+,并将底层从 javax.* 命名空间全面迁移到 jakarta.*(百度智能云, 2025, "Spring Boot 2与3版本差异深度解析", https://cloud.baidu.com/article/4467747)。这意味着所有 Servlet API 相关的类从 javax.servlet.* 变为 jakarta.servlet.*

对于 CORS 配置的直接影响:Spring 内置的 CORS 机制(@CrossOriginWebMvcConfigurerCorsFilter)本身不直接依赖 javax/jakarta 的区别,它们的 API 表面保持一致,因此对于使用标准 Spring CORS 配置的项目,Jakarta 迁移不会导致 CORS 逻辑变更。但如果项目中有自定义 Filter实现 CORS(直接实现 javax.servlet.Filter),导入包名必须从 javax.servlet.* 改为 jakarta.servlet.*,否则编译失败。

间接影响体现在 Spring Security 集成上。Spring Security 6.x(Spring Boot 3.x 搭配版本)也完成了 Jakarta 迁移,所有 Security 相关的 Filter 和 Servlet API 引用都使用 jakarta.*(腾讯云, 2025, "Spring Boot 2 和 Spring Boot 3 中使用 Spring Security 的区别", https://cloud.tencent.com/developer/article/2486669)。自定义的 Security Filter 需相应更新 import。

9.2 allowedOrigins="*" + allowCredentials=true 限制

这一限制实际上在 Spring Framework 5.3(Spring Boot 2.4.0)就已经强制生效,而非 Spring Boot 3.0 才引入。但由于 Spring Boot 2.4 之前的版本已不再被维护,当前所有活跃版本(2.4+ 和 3.x)均受此限制约束。从 2.x 早期版本(如 2.3.x)迁移到 3.x 的项目,可能需要进行 CORS 配置调整。

报错信息为:java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. Use allowedOriginPatterns instead.(CSDN/qq_73965541, 2025, "allowCredentials=true 与 allowedOrigins='*' 的冲突", https://blog.csdn.net/qq_73965541/article/details/150211871)。

迁移改动:

java
复制代码
// Spring Boot 2.3 及更早版本(已过时,2.4+ 抛异常) config.addAllowedOrigin("*"); config.setAllowCredentials(true); // Spring Boot 2.4+ / 3.x 正确写法 config.addAllowedOriginPattern("*"); config.setAllowCredentials(true);

9.3 allowedOriginPatterns 的引入

allowedOriginPatterns 从 Spring Framework 5.3(Spring Boot 2.4.0)引入(Baeldung, 2024, "CORS with Spring", https://www.baeldung.com/spring-cors),Spring Framework 6.x(Spring Boot 3.x)完全继承支持。它支持通配符模式匹配,解决了 allowedOrigins 无法与 allowCredentials 同时使用通配符的痛点。常用模式示例:

  • "*":匹配所有源(配合 allowCredentials=true 安全使用,框架会精确回显 Origin)
  • "https://*.example.com":匹配所有 example.com 子域
  • "https://*.example.com:8080":匹配特定端口
  • "https://app1.example.com, https://app2.example.com":在注解中使用逗号分隔多源

9.4 Spring Security 5.x → 6.x 配置语法变化

Spring Security 6.x(Spring Boot 3.x)废弃了 5.x 的链式 DSL 写法,推荐 Lambda 风格:

java
复制代码
// Spring Security 5.x(Spring Boot 2.x)写法 — 已废弃 @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated(); } // Spring Security 6.x(Spring Boot 3.x)写法 — 推荐 @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ); return http.build(); }

关键变化点:antMatchers()requestMatchers()authorizeRequests()authorizeHttpRequests()WebSecurityConfigurerAdapter 被移除,全部改为 SecurityFilterChain Bean 方式配置。这些变化不影响 CORS 的核心配置(.cors() 的语义不变),但需要适配新的 DSL 语法。


第十章 性能优化与架构考量

10.1 预检请求对 API 延迟的影响

预检请求为每个复杂的跨域请求增加了一次额外的 OPTIONS 往返。根据网络条件,一次往返通常需要 1-2 个 RTT(Round-Trip Time)。对于高频 API 调用,如果每次都触发预检,累积的延迟开销不可忽视。

然而,预检请求的开销主要发生在首次请求——通过 Access-Control-Max-Age 的合理配置,后续同类请求可跳过预检。maxAge 指定预检结果可被浏览器缓存的秒数,在此期间,对同一来源的相同跨域请求(方法+头组合),浏览器不会再次发送预检请求。

10.2 maxAge 配置建议

不同浏览器对 maxAge 的上限有不同限制:Chrome 的上限在 v76 前后分别为 600 秒(10 分钟)和 7200 秒(2 小时);Firefox 上限为 86400 秒(24 小时);Safari 上限为 604800 秒(7 天)(Mozilla, 2025, MDN CORS)。这意味着即使服务端设置更大的值,浏览器端实际缓存时间也受浏览器实现约束。

生产环境推荐:

  • 敏感 API:3600 秒(1 小时)——平衡性能和安全策略生效速度,策略变更后 1 小时内生效。
  • 公开 API:86400 秒(24 小时)——最大化缓存命中率,但策略变更生效较慢(Firefox 和 Safari 可以,Chrome 仍 2 小时上限)。
  • 开发环境:120 秒(2 分钟)——便于频繁调整策略即时生效。

10.3 减少预检请求的最佳实践

除了 maxAge 缓存,还可以从请求层面减少预检触发:

实践一:避免不必要的自定义头。 如果某些头非必需,移除以使请求变为简单请求。例如,优先使用标准 Accept 而非 X-Accept-Version

实践二:使用简单 Content-Type。 如果 API 支持,使用 text/plainapplication/x-www-form-urlencoded 而非 application/json。但现代 API 普遍需要 JSON,此实践适用性有限。

实践三:合并 API 调用。 将多个小 API 合并为一个批量 API,减少总请求数(同时减少预检数)。

实践四:相同源的代理。 前端开发环境使用 webpack-dev-server 或 Vite 的 proxy 功能将 API 请求代理到后端,使浏览器视角下跨域请求转为同源。

10.4 反向代理 vs 应用层处理 CORS

维度反向代理层(Nginx/Gateway)应用层(Spring Boot)
性能请求无需到达应用层即可完成预检响应每个请求需经过完整 Filter 链
配置集中度所有微服务共享统一 CORS 策略每个服务需独立维护
与安全框架协同需确保不与应用层 Security 冲突与 Spring Security 无缝集成
细粒度控制仅路径级注解级(@CrossOrigin
适用场景微服务/API Gateway 架构单体应用或简单架构

10.5 Spring Cloud Gateway 的 CORS 冲突处理

在微服务架构中,Spring Cloud Gateway 与下游 Spring Boot 服务同时配置 CORS 时,会产生"双重 CORS 头"问题——Gateway 和下游服务各添加一个 Access-Control-Allow-Origin 头,浏览器因"multiple values"错误拒绝请求。

推荐架构: 在 Gateway 层统一处理 CORS,下游服务移除 CORS 配置:

yaml
复制代码
# Spring Cloud Gateway application.yml spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOrigins: "https://app.example.com" allowedMethods: "*" allowedHeaders: "*" allowCredentials: true maxAge: 3600

下游服务移除所有 @CrossOriginWebMvcConfigurerCorsFilter 配置。如果下游服务也使用 Spring Security,在 Security 配置中调用 .cors(Customizer.withDefaults()) 但不提供 CorsConfigurationSource Bean——Gateway 层已处理 CORS,下游的 Security 仅需放行预检请求即可(预检请求到达下游时已无 Origin 头,或 Gateway 已剥离)。


第十一章 综合分析与跨主题洞察

11.1 Spring CORS 体系的分层设计哲学

纵观 Spring Framework 的 CORS 实现,可以清晰地看到"关注点分离"(Separation of Concerns)的设计哲学。CORS 处理被同时部署在 Servlet Filter 层(CorsFilter)和 MVC Handler 层(CorsInterceptor/PreFlightHandler)两个位置,这种双轨设计并非冗余,而是服务于不同场景:CorsFilter 适用于安全框架集成场景,它可以在请求到达 DispatcherServlet 之前就完成 CORS 处理;CorsInterceptor 适用于纯 Spring MVC 场景,它能够访问 Handler 级别的 @CrossOrigin 注解信息,实现细粒度配置合并。DefaultCorsProcessor 作为两个执行路径的共享内核,统一了 CORS 校验逻辑,避免了行为不一致的风险。这种"多层部署 + 共享内核"的架构,使 Spring 能够在从简单 Web 应用到复杂微服务的各种场景下保持 CORS 行为的一致性和可预测性。

11.2 凭证传递的多层防线

跨域凭证传递的安全防线不止一道,理解这些防线的层次性对正确配置至关重要。第一道防线是 CORS 协议本身——allowCredentials=true 时的精确 Origin 匹配要求。第二道防线是浏览器的 SameSite Cookie 策略——Chrome 80+ 默认 Lax 策略。第三道防线是 Spring Security 的过滤链顺序——CorsFilter 必须在认证过滤器之前。这三道防线相互补充:即使 CORS 配置正确,如果 Cookie 缺少 SameSite=None; Secure,跨域凭证仍无法传递;即使 Cookie 配置正确,如果 Security 未调用 .cors(),预检请求仍会被拦截。开发者需要同时关注这三层配置,任何一层的缺失都会导致跨域凭证传递失败。

11.3 OAuth2/JWT 与 Cookie Session 的 CORS 友好性差异

一个值得强调的实践结论是:使用 Bearer Token(通过 Authorization 头传递)的认证模式在跨域场景下比 Cookie Session 模式更为简洁。Token 不受 SameSite 限制,也不需要 Cookie 跨域配置,只要 CORS 允许 Authorization 头即可。这降低了配置复杂度和踩坑概率。对于必须使用 Cookie Session 的场景(如传统的服务端渲染架构),则需要同时处理 CORS 凭证配置和 Cookie SameSite 配置,复杂度显著更高。这一差异也是前后端分离架构在 JWT 认证模式下更为常见的原因之一。

11.4 微服务架构下的 CORS 治理

在微服务架构中,CORS 配置的治理需要"单一责任原则"——整个请求链路上应只有一个组件负责 CORS 处理。违反这一原则的最常见后果是"双重 CORS 头"导致的浏览器拒绝。推荐架构是在 API Gateway 层集中处理 CORS,下游服务不再配置 CORS(或仅保留 Spring Security 的 .cors() 以放行预检请求)。这种集中式治理不仅避免了冲突,还使 CORS 策略变更可以在一个位置统一执行,运维效率更高。


第十二章 研究局限与未来方向

12.1 研究局限

本研究主要基于公开可获取的技术文档、源码分析和社区实践报告,未涉及 Spring Framework 源码的逐行审计或大规模生产环境的实证测量。在 CORS 安全风险章节引用的奇安信/清华大学测量研究虽然提供了量化的不安全配置比例,但其测量时间点和样本范围可能不完全代表当前最新状况。此外,Spring Security 7.x 的 PreFlightRequestHandler 机制在调研时仍处于演进中,相关公开资料有限,本报告对其的描述基于已有信息和趋势推断,实际发布版本可能有差异。

12.2 需要进一步研究的领域

以下几个方向值得进一步深入调研:第一,Spring Framework 6.x 对 GraalVM Native Image 的支持对 CORS 配置的影响——Native Image 的静态分析可能影响 CorsAutoConfiguration 的自动配置行为。第二,HTTP/3(QUIC)协议下预检请求的行为是否有差异——QUIC 的连接复用可能改变预检请求的延迟模型。第三,CORS 与 Subresource Integrity(SRI)等新兴 Web 安全机制的交互。第四,新兴的 Cross-Origin-Embedder-Policy(COEP)和 Cross-Origin-Opener-Policy(COOP)等安全头与 CORS 的协同配置最佳实践。


第十三章 结论与实践建议

13.1 核心结论

Spring Boot 3.x 为 CORS 处理提供了成熟且全面的内置支持,通过分层架构(Filter 层 + MVC 拦截器层)和共享校验内核(DefaultCorsProcessor)实现了灵活性与一致性的统一。四种配置方式(@CrossOriginWebMvcConfigurerCorsFilter、Spring Security .cors())覆盖了从方法级到全局、从纯 MVC 到安全集成的各种场景,开发者应根据项目复杂度和安全框架依赖选择合适的方式或组合。

跨域凭证传递是 CORS 最复杂也最容易出错的领域,涉及 CORS 协议、Cookie SameSite 策略和 Spring Security 过滤链顺序三层配置。allowedOrigins="*" + allowCredentials=true 的限制是 W3C 规范的硬性要求,Spring 从 2.4.0 起强制执行,allowedOriginPatterns 是推荐的安全替代方案。

CORS 配置不当构成实质性的安全风险——27.5% 的 CORS 网站存在不安全配置,最危险的是 Origin 反射。安全审计应重点关注 Origin 反射、白名单宽度、凭证模式下的头暴露和 null Origin 处理。

13.2 实践建议清单

配置选择:

  1. 纯 MVC 项目:WebMvcConfigurer.addCorsMappings() 全局配置 + @CrossOrigin 个别微调。
  2. Spring Security 项目:必须 .cors(Customizer.withDefaults()) + CorsConfigurationSource Bean。
  3. 微服务架构:Gateway 层集中处理 CORS,下游服务移除 CORS 配置。

凭证模式:

  1. allowCredentials=true 时,使用 allowedOriginPatterns 替代 allowedOrigins("*")
  2. Cookie 跨域传递需同时设置 SameSite=None; Secure
  3. 优先考虑 Bearer Token + Authorization 头模式,规避 Cookie 跨域复杂性。

安全防护:

  1. allowedOrigins 使用明确的白名单,避免过宽的通配符(如 *.com)。
  2. 拒绝 null Origin。
  3. 定期审计 exposedHeaders,避免暴露敏感内部头。
  4. maxAge 生产环境建议 3600-86400 秒。

迁移适配:

  1. Spring Boot 2.x → 3.x:javax.*jakarta.*(自定义 Filter 需更新 import)。
  2. Spring Security 5.x → 6.x:链式 DSL → Lambda DSL,antMatchersrequestMatchers
  3. 迁移后检查 allowedOrigins("*") + allowCredentials(true) 配置,改为 allowedOriginPatterns("*")

参考文献

编号作者/组织日期标题URL质量评级
1Mozilla2025-11-30Cross-Origin Resource Sharing (CORS) — HTTP | MDNhttps://developer.mozilla.org/en-US/docs/Web/HTTP/CORSA
2WHATWG2024Fetch Standard — CORS Protocolhttps://fetch.spec.whatwg.org/A
3Spring (VMware/Broadcom)2024CORS — Spring Framework Referencehttps://docs.spring.io/spring-framework/reference/web/webmvc-cors.htmlA
4Spring (VMware/Broadcom)2024CORS — Spring Security Reference 7.1https://docs.spring.io/spring-security/reference/servlet/integrations/cors.htmlA
5Baeldung2024-05-11CORS with Springhttps://www.baeldung.com/spring-corsB
6百度智能云2025-10-24Spring Boot 2与3版本差异深度解析:技术演进与迁移指南https://cloud.baidu.com/article/4467747B
7腾讯云2025-01-13Spring Boot 2 和 Spring Boot 3 中使用 Spring Security 的区别https://cloud.tencent.com/developer/article/2486669B
8CSDN/qq_739655412025-08-11Spring Boot 跨域配置踩坑记:allowCredentials=true 与 allowedOrigins='*' 的冲突https://blog.csdn.net/qq_73965541/article/details/150211871C
9出门向左2017spring MVC cors跨域实现源码解析https://www.cnblogs.com/leftthen/p/6378090.htmlB
10百度开发者中心2024SpringBoot中使用SpringSecurity时CorsFilter配置不生效的原因与解决方法https://developer.baidu.com/article/detail.html?id=2767051B
11CSDN/ximeneschen2022@CrossOrigin及其实现跨域原理https://blog.csdn.net/cristianoxm/article/details/124840435C
12CSDN/好运仔dzl2025SpringBoot源码解析(二十三):跨域处理CorsFilter的自动注册原理https://blog.csdn.net/qq_50954361/article/details/148469418C
13CSDN/Eward-an2026前端跨域进阶:CORS实战避坑https://blog.csdn.net/an524415864/article/details/161681018C
14奇安信/清华大学CORS 安全大规模测量研究(CVE-2018-8014 相关)学术文献A
15Spring Projects GitHub持续更新spring-framework 源码仓库https://github.com/spring-projects/spring-frameworkA

附录 A:配置速查表

A.1 快速选型决策树

text
复制代码
项目中有 Spring Security? ├─ 是 → 在 SecurityFilterChain 中调用 .cors(Customizer.withDefaults()) │ └─ 提供 CorsConfigurationSource Bean(或复用 Spring MVC 配置) │ └─ 需要方法级微调? → 加 @CrossOrigin │ └─ 否 → 需要路径级统一配置? ├─ 是 → WebMvcConfigurer.addCorsMappings() │ └─ 需要方法级微调? → 加 @CrossOrigin └─ 否 → 需要在 Filter 层控制? ├─ 是 → 注册 CorsFilter Bean └─ 否 → 直接用 @CrossOrigin(少量接口)

A.2 常见配置组合速查

场景配置
公开 API,允许任意源allowedOrigins("*"), allowCredentials(false)
内部应用,允许特定源allowedOrigins("https://app.example.com"), allowCredentials(true)
多子域 + 凭证allowedOriginPatterns("https://*.example.com"), allowCredentials(true)
所有源 + 凭证(安全模式)allowedOriginPatterns("*"), allowCredentials(true)
微服务 GatewayGateway 层配置 CORS,下游移除

A.3 maxAge 推荐值

环境maxAge(秒)说明
开发环境120便于频繁调整策略
敏感 API 生产36001 小时,平衡性能与安全
公开 API 生产8640024 小时,最大化缓存

注意:Chrome 上限 7200 秒,Firefox 上限 86400 秒,Safari 上限 604800 秒。


附录 B:源码关键类索引

类名模块作用
AbstractHandlerMappingspring-webmvcCORS 处理入口,getHandler() 方法
CorsUtilsspring-webisCorsRequest(), isPreFlightRequest() 判断方法
CorsConfigurationspring-webCORS 配置模型,combine() 合并逻辑
UrlBasedCorsConfigurationSourcespring-web基于 URL 模式的配置源
DefaultCorsProcessorspring-web校验内核,handleInternal() 三段式校验
CorsInterceptorspring-webmvcMVC 拦截器层 CORS 处理
CorsFilterspring-webServlet Filter 层 CORS 处理
AbstractHandlerMapping.PreFlightHandlerspring-webmvc预检请求专用 Handler
CorsAutoConfigurationspring-bootSpring Boot 自动配置
CorsConfigurationSourcespring-securitySecurity 集成配置源接口

附录 C:三阶段调研验证总结

核心声明验证结果共识强度关键来源
Spring MVC HandlerMapping 内置 CORS(DefaultCorsProcessor 统一处理)验证通过强共识Spring 官方文档 + 源码分析[3][9]
allowedOrigins="*" + allowCredentials=true 抛 IllegalArgumentException(Spring Boot 2.4+)验证通过极强共识7+ 来源 + W3C 规范[1][2][5][8]
Spring Security 必须调用 .cors(),否则预检 401/403验证通过强共识Spring Security 文档 + FilterChainProxy 分析[4][10]
allowedOriginPatterns 自 Spring Boot 2.4.0 引入验证通过中高共识Baeldung + Spring 文档[3][5]
Cookie 跨域需 SameSite=None;Secure + CORS 双层配置验证通过极强共识MDN + Chrome 80 变更 + 实践报告[1][13]
maxAge:Chrome 上限 7200 秒,Firefox 86400 秒验证通过极强共识MDN 官方文档[1]
Gateway 与下游双重 CORS 配置导致重复头验证通过强共识多实践报告 + 解决方案验证

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP