彻底搞懂 CORS:跨域请求的预检与解决方案

为什么会有跨域问题?

要理解 CORS,先得理解浏览器为什么要限制跨域请求。这一切的根源是同源策略(Same-Origin Policy)。

同源策略是浏览器最核心的安全机制之一。所谓“同源”,指的是协议、域名、端口三者完全相同。比如 https://example.com:443http://example.com:443 不同源(协议不同),https://api.example.comhttps://www.example.com 也不同源(域名不同)。

没有同源策略的世界是什么样的?你在浏览器里打开了一个恶意网站,这个网站上的 JavaScript 可以随意向 https://your-bank.com 发起请求——由于浏览器会自动携带该银行的 Cookie,恶意脚本就能读取你的账户信息、发起转账。同源策略正是为了阻止这种攻击而存在的。

但同源策略过于严格,现代 Web 应用常常需要从不同源的服务器获取数据(比如前端部署在 app.example.com,API 部署在 api.example.com)。CORS(Cross-Origin Resource Sharing,跨源资源共享)就是一套在安全前提下允许跨域请求的标准化机制。

简单请求与预检请求

浏览器将跨域请求分为两类,处理方式截然不同。

简单请求

满足以下所有条件的请求属于简单请求:

  • 请求方法是 GETHEADPOST
  • 请求头仅包含安全列表字段(如 AcceptAccept-LanguageContent-LanguageContent-Type
  • 如果设置了 Content-Type,其值只能是 text/plainmultipart/form-dataapplication/x-www-form-urlencoded

简单请求的流程很直接:浏览器直接发出请求,并在请求头中自动附加 Origin 字段。服务器检查该来源是否允许,如果允许,就在响应头中返回 Access-Control-Allow-Origin。浏览器看到这个头且值匹配,就把响应交给 JavaScript;否则拦截响应并报错。

注意一个关键细节:请求实际上已经到达了服务器,服务器也可能执行了副作用操作(比如删除了某条数据)。浏览器只是在响应阶段进行了拦截。这也是为什么非简单请求需要预检——要在发送真正的请求之前先确认服务器是否允许。

预检请求

当请求不满足简单请求条件时(比如使用了 PUTDELETE 方法,或者自定义了 Authorization 头),浏览器会先自动发送一个 OPTIONS 请求,这就是预检请求。

预检请求会携带几个关键头:

  • Origin:请求的来源
  • Access-Control-Request-Method:实际请求将使用的方法
  • Access-Control-Request-Headers:实际请求将携带的自定义头

服务器收到预检后,需要返回相应的许可:

  • Access-Control-Allow-Origin:允许的来源
  • Access-Control-Allow-Methods:允许的方法
  • Access-Control-Allow-Headers:允许的请求头
  • Access-Control-Max-Age:预检结果的缓存时间(秒),在缓存期内同一请求不再重复预检

如果预检通过,浏览器才会发送真正的请求。如果预检失败,真正的请求根本不会发出。

常见触发场景

以下操作几乎必然触发预检:

  • 使用 PUTDELETEPATCH 方法
  • 发送 JSON 数据:Content-Type: application/json
  • 携带自定义头,如 X-Requested-WithAuthorization
  • 使用 XMLHttpRequestwithCredentials

很多开发者遇到的“为什么我的 POST 请求变成了 OPTIONS”就是因为发送了 application/json,触发了预检。

完整解决方案

服务端配置

以 Express 为例,使用 cors 中间件:

const cors = require('cors');

app.use(cors({
  origin: ['https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
}));

如果使用 Nginx 反向代理,可以在配置中添加:

add_header Access-Control-Allow-Origin https://app.example.com;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
add_header Access-Control-Allow-Headers 'Content-Type, Authorization';
add_header Access-Control-Allow-Credentials true;

if ($request_method = OPTIONS) {
    return 204;
}

携带 Cookie 的跨域请求

如果需要在跨域请求中携带 Cookie,前端和服务端都需要额外配置:

前端

fetch('https://api.example.com/data', {
  credentials: 'include'
});

服务端

Access-Control-Allow-Origin: https://app.example.com  // 不能是 *
Access-Control-Allow-Credentials: true

这里有一个硬性限制:当 Access-Control-Allow-Credentialstrue 时,Access-Control-Allow-Origin 不能设为通配符 *,必须指定确切的来源。这是出于安全考虑——否则任何网站都能携带用户 Cookie 访问你的 API。

开发环境代理

开发阶段最省事的方案是使用开发服务器的代理功能,让请求看起来是同源的。以 Vite 为例:

// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:3000',
        changeOrigin: true
      }
    }
  }
};

这样浏览器请求的是 http://localhost:5173/api/...,由开发服务器转发到后端,完全绕过了跨域限制。

几个容易踩的坑

坑一:预检请求返回了 302 重定向。 浏览器不允许预检请求被重定向,会直接判定失败。确保 OPTIONS 请求返回 2xx 状态码。

坑二:Nginx 配置了 CORS 但后端也配了。 两个 Access-Control-Allow-Origin 头会导致浏览器报错。只在一处配置即可。

坑三:Access-Control-Allow-Origin 动态反射了请求的 Origin。 这等同于允许所有来源,如果同时开启了 credentials,等于把用户数据暴露给了任何网站。务必使用白名单校验。

坑四:预检缓存导致配置更新不生效。 Access-Control-Max-Age 设置过长时,浏览器会缓存预检结果,服务端修改了 CORS 策略后客户端可能仍然使用旧的缓存。调试时可以将其设为 0。

总结

CORS 的本质是浏览器在服务端授权的前提下,有条件地放宽同源策略。理解它的关键在于区分简单请求和预检请求两条路径,以及明确 Access-Control-Allow-Origincredentials 之间的约束关系。实际开发中,开发环境用代理、生产环境用精确的白名单配置,基本能覆盖绝大多数场景。遇到问题时,打开浏览器 Network 面板查看 OPTIONS 请求的请求头和响应头,对照本文中的规则逐一排查,绝大多数 CORS 报错都能迎刃而解。

未经允许不得转载:任鹏个人博客 » 彻底搞懂 CORS:跨域请求的预检与解决方案

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏