浏览器报错 CORS 跨域几乎是每个前后端开发者都绕不开的坎:本地跑得好好的接口,一上线就冒出红色 Access-Control-Allow-Origin 警告,预检 OPTIONS 请求被拒、带 Cookie 的请求莫名失败。本文从同源策略讲起,用一张对照表梳理最常见的 5 类报错与根因,并给出 Nginx、Spring Boot、Node 的可直接复制配置,帮你把 CORS 跨域问题一次性彻底解决。
一、CORS 到底在防什么:同源策略
同源策略(Same-Origin Policy)是浏览器的安全基石:当协议、域名、端口三者任意一个不同,就视为跨源。跨源的前端脚本默认不能读取后端响应,CORS(Cross-Origin Resource Sharing,跨域资源共享)则是服务端用一组响应头,主动声明「允许哪些源、哪些方法、哪些头部」来放宽这条限制。它不是前端能单方面绕过的,必须由服务端配合。
二、预检请求(Preflight)为何而来
对于「简单请求」,浏览器直接发正式请求、再看响应头决定是否放行;但对于可能改数据的「非简单请求」,浏览器会先发一个 OPTIONS 方法的预检请求,确认服务端真的允许,才发真实请求。理解这一点,就明白为什么控制台里凭空多出一个 OPTIONS。
哪些情况会触发 OPTIONS 预检
满足以下任一条件即触发预检:使用了 PUT、DELETE、PATCH 等非 GET/POST 方法;Content-Type 为 application/json、application/xml 等非表单类型;自定义了请求头(如 Authorization、X-Token);或带凭证且服务端返回了不匹配的头。反之,普通表单 POST(application/x-www-form-urlencoded)往往是简单请求,不会预检。
用 curl 亲手模拟一次预检
调试时最直观的方法,是绕过浏览器用 curl 复现预检,看服务端到底回了什么头:
curl -i -X OPTIONS 'https://api.example.com/v1/order' \
-H 'Origin: https://shop.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type,authorization'
# 关注返回头里是否包含:
# access-control-allow-origin: https://shop.example.com
# access-control-allow-methods: GET,POST,OPTIONS
# access-control-allow-headers: content-type,authorization三、最常见的 5 类报错与根因
把现象、根因、解法对齐,排错效率立刻翻倍:
| 现象(控制台/网络面板) | 根因 | 解法 |
|---|---|---|
| No ‘Access-Control-Allow-Origin’ header | 服务端没返回允许源 | 后端加 Access-Control-Allow-Origin |
| Method %s is not allowed | allow-methods 未包含该动词 | 补上 PUT/DELETE/OPTIONS 等方法 |
| Request header field x-token is not allowed | allow-headers 缺自定义头 | 把自定义头加入允许列表 |
| Preflight response is not successful (403/404) | 网关/反代拦截了 OPTIONS | Nginx 放行 OPTIONS 并回 204 |
| The value of ‘Expose-Headers’ / credentials 冲突 | 带凭证时 allow-origin 不能是 * | 显式写死具体源,且 allow-credentials: true |
四、服务端正确配置:三种常见栈
Nginx 加响应头(含预检放行)
在反代 location 内统一加头,并单独处理 OPTIONS 预检,直接返回 204 避免穿透到后端。若你的站点已用 Nginx 反向代理 托管多个服务,把这段放进对应 server 块即可。
location /api/ {
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin 'https://shop.example.com' always;
add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS' always;
add_header Access-Control-Allow-Headers 'content-type,authorization,x-token' always;
add_header Access-Control-Allow-Credentials 'true' always;
add_header Content-Length 0;
return 204;
}
add_header Access-Control-Allow-Origin 'https://shop.example.com' always;
add_header Access-Control-Allow-Credentials 'true' always;
proxy_pass http://127.0.0.1:8080;
}Spring Boot:全局配置优于注解
用 @CrossOrigin 注解虽快,但散落各处难维护;推荐实现 WebMvcConfigurer 做全局放行。注意 allow-credentials 为 true 时,allowedOriginPatterns 不能写 *。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOriginPatterns("https://shop.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}Node / Express 显式设置头
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://shop.example.com');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});五、带凭证的请求(credentials)坑点
当 fetch 设置 credentials: 'include' 或 axios 设 withCredentials: true 想带上 Cookie 时,规则立刻变严:Access-Control-Allow-Origin 必须写死具体源,不能是通配符 *,且必须同时返回 Access-Control-Allow-Credentials: true,前端才能拿到响应。写成 * 是新手最常踩的隐形坑——浏览器会直接丢弃响应。
// 前端:带凭证调用
fetch('https://api.example.com/v1/profile', {
method: 'GET',
credentials: 'include', // 关键:带上 Cookie
headers: { 'Content-Type': 'application/json' }
});六、开发环境用代理绕过,而非关掉安全
本地联调时,前后端端口不同天然跨域。与其在后端临时放开,不如用构建工具的反代把请求「变」成同源。Hono、Vue3 等现代前端栈都支持 devServer 代理,生产环境再用上文的服务端头兜底。
// vite.config.ts —— 本地把 /api 代理到后端,规避 CORS
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (p) => p.replace(/^\/api/, '')
}
}
}
});七、生产排错清单
| 检查项 | 要点 |
|---|---|
| 预检是否放行 | OPTIONS 必须返回 2xx,且带 allow-* 头 |
| Origin 是否精确匹配 | 带凭证时禁止用 *,写死具体域名 |
| 自定义头是否声明 | Authorization、X-Token 等需进 allow-headers |
| 网关层是否拦截 | Nginx/CDN/WAF 别把 OPTIONS 当异常丢弃 |
| 凭证一致性 | 前端 include + 后端 credentials:true 同时成立 |
| 缓存预检结果 | maxAge 设 600~3600 秒,减少重复 OPTIONS |
把这张清单过一遍,90% 的 CORS 跨域问题都能在十分钟内切掉。它和 微前端架构 的跨应用调用、以及各类 BFF 网关配置是同一套心智模型——本质都是「谁在边界上替你签发允许凭证」。
结语
CORS 不是敌人,而是浏览器替你守住的一道门。理解同源策略与预检机制后,它不过是一组需要正确填写的响应头。下次再见到红色报错,先 curl 复现预检、再对表定位,比盲目加 * 安全得多。




