前端跨域 CORS 完整指南:从预检到生产落地

做前端开发,几乎人人都遇到过浏览器控制台的红色报错:跨域 CORS 被拦截。它本质是浏览器同源策略(Same-Origin Policy)的安全护栏,而不是服务器拒绝响应。本文从原理到落地,把简单请求、预检请求(preflight)和带凭证的请求一次讲透,并给出可直接抄进项目的 Nginx、Node、Spring Boot 配置。

一、什么是跨域:同源策略的边界

同源策略规定:只有当协议、域名、端口三者完全一致时,两个 URL 才算“同源”。只要有一个不同,浏览器就会把它判定为跨域。例如页面在 https://fsdata.site,去请求 https://api.fsdata.sitehttps://fsdata.site:8080,都会触发跨域。注意:跨域是浏览器的限制,用 Postman 或 curl 直接请求并不会报 CORS 错误——这也正是调试时最容易让人困惑的地方。

二、CORS 响应头全景:一张表记牢

服务端通过在响应里带上 Access-Control-* 头,告诉浏览器“我允许这个来源访问”。下面是最常用的一组头:

响应头作用常见取值
Access-Control-Allow-Origin允许的来源https://fsdata.site 或 *
Access-Control-Allow-Methods允许的 HTTP 方法GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers允许的自定义请求头Authorization, Content-Type
Access-Control-Allow-Credentials是否允许带 Cookietrue / false
Access-Control-Max-Age预检结果缓存秒数86400

三、简单请求 vs 预检请求:决定成败的分水岭

浏览器把跨域请求分成两类。满足以下全部条件的是“简单请求”,直接发,不带预检:方法是 GET/POST/HEAD;仅含安全头(Accept、Content-Type 等);且 Content-Type 只能是 application/x-www-form-urlencodedmultipart/form-datatext/plain 三者之一。

维度简单请求预检请求
触发条件方法/头/类型均受限JSON body、自定义头、PUT/DELETE
实际请求前不发 OPTIONS先发 OPTIONS 探路
服务端额外要求仅需 Allow-Origin需 Allow-Methods / Allow-Headers

下面这种带 JSON body 的写法,必然触发预检,因为 Content-Type: application/json 不在简单请求白名单内:

fetch('https://api.fsdata.site/v1/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'hello' })
});

浏览器会先悄悄发一个 OPTIONS 请求,带上 Access-Control-Request-MethodAccess-Control-Request-Headers,只有服务端返回 2xx 且头正确,真正的 POST 才会发出。

四、服务端怎么配:三套可抄配置

4.1 Nginx 反向代理层放行

最常见也最省心的方式是在网关层统一加头。下面这段可直接放进 Nginx 的 location 块(和本站 Nginx 基础配置Nginx 反向代理实战 的写法一脉相承):

location /api/ {
    add_header 'Access-Control-Allow-Origin' '$http_origin' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;

    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Max-Age' 86400;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        return 204;
    }
}

4.2 Node / Express 中间件

Node 服务用官方 cors 包最稳妥,一行即可;需要精细控制时传配置对象:

const cors = require('cors');
app.use(cors({
  origin: 'https://fsdata.site',
  credentials: true
}));

4.3 Spring Boot 全局配置

Spring Boot 推荐实现 WebMvcConfigurer 做全局映射,避免在每个 Controller 上散落 @CrossOrigin

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://fsdata.site")
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

五、带凭证的跨域:withCredentials 的坑

前端如果要携带 Cookie(登录态),必须两端同时配合:请求侧加 credentials: 'include',且服务端返回 Access-Control-Allow-Credentials: true关键约束:一旦允许凭证,Access-Control-Allow-Origin不能写 *,必须写明具体来源,否则浏览器直接拒绝。

fetch('https://api.fsdata.site/v1/me', {
  credentials: 'include'   // 必须与服务端 Allow-Credentials: true 配对
});

六、生产环境 Checklist 与常见报错

上线前对照这张表排查,能解决 90% 的 CORS 工单:

现象根因修复
OPTIONS 返回 403/404网关未放行 OPTIONS 方法Nginx 对 OPTIONS 返回 204
Allow-Origin 不匹配写了 * 但带了凭证改为具体域名
自定义头仍被拦Allow-Headers 没列全补全 Authorization 等
预检频繁耗时未设 Max-Age设 86400 缓存

七、前端兜底:代理与 JSONP 的取舍

开发阶段最干净的方案是本地代理:在 Vue3 项目vite.config.ts 里配 server.proxy,把 /api 转发到后端,浏览器侧完全无跨域。至于 JSONP,它只能发 GET 且依赖后端改造,如今已被 CORS 全面取代,新项目不建议再引入。

结语

跨域 CORS 不是玄学,而是一套“浏览器问、服务端答”的协商协议。记住三件事:简单请求看类型、复杂请求必预检、带凭证不能写星号。把本文的 Nginx / Node / Spring Boot 配置按自己域名改一改,就能从报错走向上线。

上一篇 文件描述符耗尽导致服务假死:一次 fd 泄漏排查实录
下一篇 Spring Boot 异步线程池:@Async 配置与避坑实战