bigywater.tech
Spring · 50

CORS:预检请求与配置的三个位置

浏览器报 CORS 错误,服务端到底返回了什么?预检请求和真正的请求分别带哪些头?@CrossOrigin、addCorsMappings、CorsFilter 三个位置有什么区别?为什么 500 和 404 在浏览器里总是变成 CORS 错误?这篇在 Spring Boot 4.0.8 上用手工带 Origin 的请求逐个看响应头。

403
不在白名单的来源,实际请求和预检请求都被拒绝,方法不执行
1800
没写 maxAge 时预检响应的默认 Access-Control-Max-Age(秒)
0
没有匹配 CORS 配置的路径上,500 和 404 响应里的 CORS 头个数

无配置

实测 · 本机执行输出
  GET /plain  [Origin: https://a.example] → 200  body='plain'
      CORS 响应头:(没有)
  GET /plain  [] → 200  body='plain'
      CORS 响应头:(没有)
  OPTIONS /plain  [Origin: https://a.example Req-Method: GET] → 200  body=''
      CORS 响应头:(没有)

服务端照常返回,拦住读取的是浏览器;CORS 不是访问控制。

@CrossOrigin

实测 · 本机执行输出
  GET /ann  [Origin: https://a.example] → 200  body='ann'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://a.example'}
  GET /ann  [Origin: https://evil.example] → 403  body='Invalid CORS request'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']}
  GET /ann  [] → 200  body='ann'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']}
  GET /ann  [Origin: http://127.0.0.1:9602] → 200  body='ann'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']}
  OPTIONS /ann  [Origin: https://a.example Req-Method: GET] → 200  body=''
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://a.example', 'Allow-Methods': 'GET', 'Max-Age': '1800'}
  OPTIONS /ann  [Origin: https://evil.example Req-Method: GET] → 403  body='Invalid CORS request'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']}
  OPTIONS /ann  [Origin: https://a.example Req-Method: DELETE] → 200  body=''
      CORS 响应头:(没有)

白名单外的来源 403;预检与实际请求带的头不同;同源不算跨域。

属性与默认值

实测 · 本机执行输出
  OPTIONS /ann-put  [Origin: https://a.example Req-Method: PUT Req-Headers: X-Token] → 200  body=''
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://a.example', 'Allow-Methods': 'GET,PUT', 'Allow-Headers': 'X-Token', 'Expose-Headers': 'X-Total', 'Allow-Credentials': 'true', 'Max-Age': '600'}
  OPTIONS /ann-put  [Origin: https://a.example Req-Method: PUT Req-Headers: X-Other] → 403  body='Invalid CORS request'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']}
  PUT /ann-put  [Origin: https://a.example] → 200  body='ann-put'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://a.example', 'Expose-Headers': 'X-Total', 'Allow-Credentials': 'true'}
  GET /open  [Origin: https://any.example] → 200  body='open'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': '*'}
  OPTIONS /open  [Origin: https://any.example Req-Method: GET Req-Headers: X-Whatever] → 200  body=''
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': '*', 'Allow-Methods': 'GET', 'Allow-Headers': 'X-Whatever', 'Max-Age': '1800'}

maxAge 默认 1800;空注解允许任意来源。

全局配置

实测 · 本机执行输出
  OPTIONS /api/x  [Origin: https://b.example Req-Method: POST Req-Headers: Content-Type] → 200  body=''
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://b.example', 'Allow-Methods': 'GET,POST', 'Allow-Headers': 'Content-Type', 'Max-Age': '120'}
  OPTIONS /api/x  [Origin: https://b.example Req-Method: DELETE] → 403  body='Invalid CORS request'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']}
  GET /api/x  [Origin: https://b.example] → 200  body='api-x'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://b.example'}
  GET /plain  [Origin: https://sub.example] → 200  body='plain'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://sub.example', 'Allow-Credentials': 'true'}
  GET /plain  [Origin: https://a.other] → 403  body='Invalid CORS request'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']}
  GET /ann  [Origin: https://a.example] → 200  body='ann'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://a.example'}
  GET /api/boom  [Origin: https://b.example] → 500  body='{"status":500,"error":"Internal Server Error","pat'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://b.example'}

allowedOriginPatterns 回显来源;注解仍然生效。

CorsFilter

实测 · 本机执行输出
  GET /boom  [Origin: https://f.example] → 500  body='{"status":500,"error":"Internal Server Error","pat'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://f.example'}
  GET /nope  [Origin: https://f.example] → 404  body='{"status":404,"error":"Not Found","path":"/nope"}'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://f.example'}
  OPTIONS /boom  [Origin: https://f.example Req-Method: POST Req-Headers: X-Any] → 200  body=''
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://f.example', 'Allow-Methods': 'POST', 'Allow-Headers': 'X-Any'}
  GET /boom  [Origin: https://other.example] → 403  body='Invalid CORS request'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers']}

在 DispatcherServlet 前,404 和异常响应都带头。

没有配置的路径

实测 · 本机执行输出
  GET /boom  [Origin: https://a.example] → 500  body='{"status":500,"error":"Internal Server Error","pat'
      CORS 响应头:(没有)
  GET /nope  [Origin: https://a.example] → 404  body='{"status":404,"error":"Not Found","path":"/nope"}'
      CORS 响应头:(没有)
  GET /ann-boom  [Origin: https://a.example] → 500  body='{"status":500,"error":"Internal Server Error","pat'
      CORS 响应头:{'Vary': ['Origin', 'Access-Control-Request-Method', 'Access-Control-Request-Headers'], 'Allow-Origin': 'https://a.example'}

没有匹配 CORS 配置的路径上,500 和 404 不带 CORS 头。

配置错误

实测 · 本机执行输出
  GET /plain  [Origin: https://a.example] → 500  body='<!doctype html><html lang="en"><head><title>HTTP S'
      CORS 响应头:(没有)
  OPTIONS /plain  [Origin: https://a.example Req-Method: GET] → 500  body='<!doctype html><html lang="en"><head><title>HTTP S'
      CORS 响应头:(没有)
  服务端日志:Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: 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. To allow crede

allowedOrigins("*") 加 allowCredentials(true) 启动不报错,请求时 500。

分步演示

CORS
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8 的 SpringApplication(spring-boot-starter-webmvc,内嵌 Tomcat 11.0.24);请求由 Python 的 http.client 手工带 Origin、Access-Control-Request-* 头发出,表中的头名省略了 Access-Control- 前缀(Vary 的多个值合并显示);各配置位置用 @ConditionalOnProperty 按属性开关,分别启动应用
    • 没有验证:真实浏览器对这些响应头的处理(包括私有网络访问、缓存预检结果);Spring Security 开启后 CORS 与过滤器链的顺序(见 Security 章节);Access-Control-Allow-Private-Network;allowedOriginPatterns 的更复杂写法;WebFlux 的 CorsWebFilter;Origin: null 的处理
    京ICP备2026067502号-1京公网安备11010502063487号