网络与安全
跨域与 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 头。
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,要在前端明确声明:
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 的写法,只放行白名单:
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。自定义的头需要后端声明:
Access-Control-Expose-Headers: X-Total-Count开发环境用代理
本地开发时最省事的做法是让开发服务器代理接口。对浏览器来说这是同源请求,也就不存在跨域问题。以 Vite 为例:
export default {
server: {
proxy: {
'/api': { target: 'http://localhost:3000', changeOrigin: true },
},
},
};还有一种常见的错误尝试是设置 mode: 'no-cors'。它不会解决问题,只会让 JavaScript 拿到一个读不了内容的响应。
排查清单
- 看 DevTools 的 Network 面板,有没有失败的
OPTIONS请求?状态码是多少? - 响应里
Access-Control-Allow-Origin和当前页面的 Origin 完全一致吗?协议和端口都要相同。 - 带了 Cookie 吗?如果带了,
Allow-Origin不能是*。 - 错误响应(500 等)是否也带了 CORS 头?如果没有,真正的错误会被伪装成跨域错误。nginx 的
add_header要加always才会作用于错误响应。