Rucky 前端笔记

网络与安全

跨域与 CORS:报错时到底发生了什么

跨域错误是浏览器拦下了响应,不代表请求没有到达服务器。弄清这一点,再看预检和 Cookie 的规则就顺理成章了。

什么算跨域

协议、域名、端口三者完全相同才算同源。以 https://a.example.com 为例:

地址是否同源原因
https://a.example.com/page是只有路径不同
http://a.example.com否协议不同
https://b.example.com否域名不同
https://a.example.com:8443否端口不同

最重要的一点:拦截发生在浏览器

看到 CORS 报错时,容易以为请求被挡住了,但在很多情况下,请求已经到达服务器并且被处理了,只是浏览器发现响应里没有允许的标头,于是不把结果交给 JavaScript。

这能解释几个常见现象:

  • 用 curl 或 Postman 调用接口完全正常,因为它们不执行同源策略。
  • 接口报了跨域错误,但数据库里的数据却已经改了。
  • CORS 不是服务器的安全防护。它保护的是用户,防止恶意网站读取用户在其他网站上的数据。防 CSRF 不能指望它。

简单请求和预检

不是所有跨域请求都直接发送。满足下面全部条件的叫“简单请求”,浏览器直接发出,再检查响应:

  • 方法是 GET、HEAD 或 POST。
  • 只使用安全列表内的请求头,没有 Authorization 这样的自定义头。
  • Content-Type 只能是 text/plain、multipart/form-data 或 application/x-www-form-urlencoded。

其他请求会先发一个 OPTIONS 预检,问服务器是否允许。最常见的触发原因,就是发送 Content-Type: application/json 或者带上 Authorization 头。

HTTP
OPTIONS /api/user HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: content-type, authorization
Access-Control-Max-Age: 600

预检必须返回 2xx 状态码。如果后端的 OPTIONS 被登录校验拦下返回 401,或者路由没有处理返回 404,真正的请求就不会发出。这是最常见的 CORS 故障之一。

Access-Control-Max-Age 让浏览器缓存预检结果,不必每次都多发一个请求。

带 Cookie 的请求规则更严格

跨域请求默认不带 Cookie,要在前端明确声明:

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

服务端则必须同时满足:

  • Access-Control-Allow-Credentials: true。
  • Access-Control-Allow-Origin 写明具体的域名,**不能是 ***。
  • 跨站场景下,Cookie 本身要设置 SameSite=None; Secure。

如果要允许多个域名,需要按请求动态返回,并且加上 Vary: Origin,防止缓存把给 A 域名的响应发给 B 域名。下面是 nginx 的写法,只放行白名单:

nginx
map $http_origin $cors_origin {
    default "";
    "https://app.example.com" $http_origin;
    "https://admin.example.com" $http_origin;
}

server {
    add_header Access-Control-Allow-Origin $cors_origin always;
    add_header Vary Origin always;
}

千万不要原样回显任意 Origin 同时允许带 Cookie,这等于允许任何网站读取用户的数据。

读不到响应头

默认情况下,JavaScript 只能读取少数几个响应头,比如 Content-Type。自定义的头需要后端声明:

HTTP
Access-Control-Expose-Headers: X-Total-Count

开发环境用代理

本地开发时最省事的做法是让开发服务器代理接口。对浏览器来说这是同源请求,也就不存在跨域问题。以 Vite 为例:

JavaScript
export default {
  server: {
    proxy: {
      '/api': { target: 'http://localhost:3000', changeOrigin: true },
    },
  },
};

还有一种常见的错误尝试是设置 mode: 'no-cors'。它不会解决问题,只会让 JavaScript 拿到一个读不了内容的响应。

排查清单

  1. 看 DevTools 的 Network 面板,有没有失败的 OPTIONS 请求?状态码是多少?
  2. 响应里 Access-Control-Allow-Origin 和当前页面的 Origin 完全一致吗?协议和端口都要相同。
  3. 带了 Cookie 吗?如果带了,Allow-Origin 不能是 *。
  4. 错误响应(500 等)是否也带了 CORS 头?如果没有,真正的错误会被伪装成跨域错误。nginx 的 add_header 要加 always 才会作用于错误响应。

延伸阅读