你有没有遇到过这种情况:在自己的网页上想请求别人的API,结果浏览器直接报错:Access-Control-Allow-Origin' header is missing。为什么浏览器要阻止你?服务器不响应不就完了吗?
今天,用小区门禁的故事,来讲讲跨域与CORS。
什么是"跨域"?
同源策略 — 浏览器的安全基石
浏览器有个同源策略(Same-Origin Policy):只有来自同一个"家"的资源才能随便用。
什么叫"同一个家"?看三个条件:协议(http/https)、域名(example.com)、端口(:8080)。三个都一样,才是同源;有一个不一样,就是跨域。
跨域的例子
1 2 3 4 5 | ✅ http://example.com 和 http://example.com/profile // 协议+域名+端口都相同 → 同源
✅ https://example.com 和 https://example.com // 协议+域名+端口都相同 → 同源
❌ http://example.com 和 https://example.com // 协议不同 → 跨域
❌ http://example.com 和 http://api.example.com // 域名不同(子域名)→ 跨域
❌ http://example.com:8080 和 http://example.com:3000 // 端口不同 → 跨域
|
跨域限制了什么?
浏览器的同源策略主要限制了三件事:
- DOM 访问:无法读取不同源的 iframe 内容、无法修改不同源的 iframe DOM
- AJAX 请求:无法请求不同源的 API
- Cookie/LocalStorage:无法访问不同源的数据
为什么要限制跨域?
模拟一个攻击场景
想象一下:你登录了银行网站,浏览器保存了你的登录 Cookie。
然后你手滑点进了一个恶意网站,这个网站里有一段代码:
1 2 3 4 5 | <form href="https://urlscan.io/result/019dd18e-1f44-74de-a5be-32c1f5ea040c/"method="POST">
<input type="hidden"name="to"value="hacker">
<input type="hidden"name="amount"value="1000000">
</form>
<script>document.forms[0].submit();</script>
|
如果没有同源策略,这个表单请求会自动带上bank.com的 Cookie,银行服务器以为是你本人操作的——钱就没了。
同源策略就是浏览器的"门禁":只有同一家人才能进,陌生人要查证件。
💡 注意:<img>标签的 GET 请求虽然也会带 Cookie,但现代浏览器有SameSiteCookie 保护。上面表单 POST 场景更典型。
CORS — 跨域的"通行证"
CORS 是什么?
CORS(Cross-Origin Resource Sharing)= 跨域资源共享。
它的工作原理很简单:让服务器告诉浏览器,"我允许来自这些源的请求"。
简单请求 vs 预检请求
简单请求
满足以下条件的请求是"简单请求":
![]()
简单请求的流程:
1 2 3 4 5 6 7 | 1. 浏览器发送请求(自动带上 Origin 头)
↓
2. 服务器检查 Origin,决定是否允许
↓
3. 服务器返回响应头 Access-Control-Allow-Origin
↓
4. 浏览器检查响应头,允许就完事
|
服务器端示例(Node.js):
1 2 3 4 5 6 7 8 9 | app.get('/api/data', (req, res) => {
constorigin = req.headers.origin;
if(origin === '') {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.json({ data:'这是返回的数据'});
});
|
响应头:
1 2 3 4 5 | HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://example.com
<a href="https://urlscan.io/result/019dd18e-29e6-702b-b9f9-d2445b252501/"></a>
Content-Type: application/json
{"data":"这是返回的数据"}
|
预检请求(Preflight)
不满足"简单请求"条件的,浏览器会先发一个 OPTIONS 请求"探路":
1 2 3 4 5 6 7 | 1. 浏览器发送 OPTIONS 预检请求
↓
2. 服务器检查方法/头部/Origin
↓
3. 服务器返回允许的头 Access-Control-*
↓
4. 浏览器发送实际请求
|
预检请求检查什么?
预检请求(OPTIONS)就像登机前的安检——先检查你带没带危险品。
浏览器会问服务器三件事:
- 我从哪来?(Origin)
- 我想用什么方法?(Access-Control-Request-Method)
- 我想带什么头?(Access-Control-Request-Headers)
服务器回答"可以",浏览器才放行实际请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | # 请求(浏览器发给服务器)
OPTIONS /api/data HTTP/1.1
Origin: <a href="https://urlscan.io/result/019dd18e-35a3-76ab-99bf-0752d3378713/"></a> # 我从哪来
Access-Control-Request-Method: PUT # 我想用 PUT 方法
Access-Control-Request-Headers: Content-Type, Authorization # 我想带这些头
---
# 响应(服务器告诉浏览器)
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: <a href="https://urlscan.io/result/019dd18e-4068-70d2-a85c-2bfa345b23aa/"></a> # 允许这个源
Access-Control-Allow-Methods: GET, POST, PUT, DELETE # 允许这些方法
Access-Control-Allow-Headers: Content-Type, Authorization # 允许这些头
Access-Control-Max-Age: 86400 # 预检结果缓存24小时
|
服务器端处理:
1 2 3 4 5 6 7 8 9 10 11 12 | app.options('/api/data', (req, res) => {
constorigin = req.headers.origin;
if(origin === '<a href="https://urlscan.io/result/019dd18e-4c5a-71cc-b20b-a4e8ac2ec164/"></a>') {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods','GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers','Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age','86400');
}
res.status(204).send();
});
|
CORS 响应头详解
常用响应头
credentials 模式
默认情况下,CORS不带Cookie。如果需要携带 Cookie:
前端:
1 2 3 | fetch('<a href="https://urlscan.io/result/019dd18e-58c4-72c9-b486-e6c5ab731092/"></a>', {
credentials:'include'
});
|
服务端:
1 2 | res.setHeader('Access-Control-Allow-Origin', '<a href="https://urlscan.io/result/019dd18e-64fb-73dd-b51a-0601024a892f/"></a>');
res.setHeader('Access-Control-Allow-Credentials','true');
|
注意:Access-Control-Allow-Origin不能用*,必须是具体域名。
跨域的解决方案
1. JSONP(已不推荐)
利用<script>标签不受同源策略限制的特性:
1 2 3 4 5 6 | <script>
function handleData(data) {
console.log(data);
}
</script>
<a href="https://urlscan.io/result/019dd18e-700f-705d-a721-278d4d15ac80/"></a>
|
2. 代理服务器
在自己的服务器上转发请求,"伪装"成同源:
1 | 浏览器 ──> 我的服务器(同一源) ──> 目标服务器
|
Nginx 代理:
1 2 3 | location /api/ {
<a href="https://urlscan.io/result/019dd18f-3875-71a6-a17d-cffb86fc9a00/"></a>;
}
|
Node.js 代理:
1 2 3 4 5 | app.get('/api/data', async (req, res) => {
constresponse = await fetch('<a href="https://urlscan.io/result/019dd18f-454d-7307-bbb2-ab0a62f529b5/"></a>');
constdata = await response.json();
res.json(data);
});
|
3. Webpack/Vite 开发代理
开发环境配置代理:
1 2 3 4 5 6 7 8 9 10 11 | // vite.config.js
exportdefault{
server: {
proxy: {
'/api': {
target:'http://target-server.com',
changeOrigin:true
}
}
}
};
|
4. postMessage
不同窗口/iframe 之间的通信:
1 2 3 4 5 6 7 | window.addEventListener('message', (event) => {
if(event.origin ==='https://example.com') {
console.log('收到消息:',event.data);
}
});
iframe.contentWindow.postMessage('hello','https://example.com');
|
深入了解 CORS 🔬
第三方 Cookie 的限制
现代浏览器正在逐步限制第三方 Cookie:
CORS 和 CSRF 的区别
为什么 OPTIONS 叫"预检"?
"预检"就像登机前的安检——先检查你带没带危险品(方法、头部),没问题了才让你登机(发送实际请求)。
常见错误排查
错误 1:No 'Access-Control-Allow-Origin' header
错误 2:Method not allowed
错误 3:Header not allowed
错误 4:预检请求 404
总结
写在最后
现在你应该明白了:
- 跨域是浏览器的安全机制,不是为了刁难你
- CORS 是服务器授权机制,服务器说可以,浏览器才放行
- 预检请求= 安检,OPTIONS 通过了才能发送实际请求
- 生产环境推荐用代理,开发环境用 webpack/vite 代理
下次遇到跨域错误,先看浏览器控制台的报错信息——是"缺通行证"(header 缺失)还是"通行证不对"(origin 不匹配),处理方式不一样的。