一、问题:浏览器为什么拦了我的请求?
你有没有遇到过这样的情况?
你在本地开发一个网页,地址是 http://localhost:5173,想从 http://localhost:8080 这个地址获取一些数据。你写好了代码,点击运行,结果浏览器控制台跳出一行红色警告:
Access to fetch at 'http://localhost:8080/api/hello' from origin 'http://localhost:5173' has been blocked by CORS policy
翻译过来就是:“你不能从这个域名访问那个域名的数据,这是规定。”
这堵“墙”就是 同源策略(Same-Origin Policy) 。
二、什么是“同源”?为什么需要?
“同源” = 协议 + 域名 + 端口 都相同。
比如:
| URL | 是否同源 |
|---|---|
http://localhost:5173 | ✅ 同源 |
http://localhost:8000 | ❌ 不同源(端口不同) |
https://localhost:5173 | ❌ 不同源(协议不同) |
http://127.0.0.1:5173 | ❌ 不同源(IP 虽等价,但字符串不同) |
这就像你住在 A 小区,却想进 B 小区翻别人家的抽屉——门卫(浏览器)必须拦你。
确保一个网站的脚本只能访问自身域名下的资源,防止恶意网站通过脚本读取或篡改其他网站的数据,如用户会话信息或敏感数据,从而保护用户隐私和数据安全,避免跨站脚本攻击(XSS)等安全隐患。这一策略是浏览器安全的基石。
但问题是:现代 Web 应用,前端和后端本来就在不同服务器上啊!
比如:
- 前端:
https://www.myapp.com(用户访问) - 后端 API:
https://api.myapp.com或http://localhost:8000(处理数据)
如果完全不能跨域,那网页岂不是“断网”了?
于是,浏览器说:“我可以让你跨,但必须双方都同意。”
这个“同意机制”,就是 CORS(Cross-Origin Resource Sharing,跨域资源共享) 。
三、第一代解决方案:JSONP——用“脚本”绕过检查
1. 核心思路:利用 <script> 标签不受同源策略限制
虽然浏览器禁止 fetch 或 XMLHttpRequest 跨域请求数据,但它允许你加载外部的 JavaScript 脚本。比如你可以:
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
这个脚本来自另一个域名,但浏览器允许加载。
小记:script脚本,Link,img都可以跨域
能不能利用这一点来“偷渡”数据呢?
答案是:能!这就是 JSONP 的诞生。
2. JSONP 是怎么工作的?
一个生活化的比喻
想象你和朋友约定好一种“暗号通信”方式:
-
你写一封信,信封上写:
“请把‘牛奶已送到’这句话,用‘收到货(……)’的方式告诉我。”
-
朋友收到信后,按约定写回:
收到货("牛奶已送到")
你一看到这行字,就知道货到了。
在技术世界里:
- 你 = 前端
- 朋友 = 后端
- 信 =
<script>请求 - “收到货” = 回调函数名(callback)
- “牛奶已送到” = 真实数据
3. 代码实战:前后端如何配合
前端代码(发“信”)
// 创建一个叫 'show' 的全局函数
window.show = function(data) {
console.log("服务器说:", data);
};
// 动态创建一个 script 标签
const script = document.createElement('script');
script.src = 'http://localhost:5173/say?wd=ilikeyou&callback=biaobaiCallback';
document.body.appendChild(script);
这相当于说:“请把数据用 biaobaiCallback(...) 包着发给我。”
后端代码(回“信”)
const http = require('http');
const server = http.createServer((req, res) => {
// 匹配 GET 请求 /say
if (req.url.startsWith('/say')) {
const url = new URL(req.url, `http://${req.headers.host}`);
const wd = url.searchParams.get('wd');
const callback = url.searchParams.get('callback');
console.log(url);
console.log(wd); // Iloveyou
console.log(callback); // show
// 返回 JSONP 格式响应
res.writeHead(200, { 'Content-Type': 'application/javascript' });
res.end(`${callback}('我不爱你')`);
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});
打印结果:
后端收到请求后,返回的不是普通数据,而是一段JavaScript 代码:
show({"data": "我不爱你"})
浏览器一执行这段代码,就会调用你定义的 show 函数,数据就到手了!
API解析:
new URL(relative, base)是 ES6+ 提供的构造 URL 对象的方法。req.url是相对路径(如/say?wd=hello)。http://${req.headers.host}是基础 URL(例如:http://example.com),req.headers.host包含了客户端请求的主机名和端口(如localhost:3000)。- 这行代码将相对路径和基础 URL 组合,生成一个完整的、可解析的
URL对象。 - 有了这个对象,就可以方便地提取查询参数(query parameters)。
举个例子:如果
req.url是/say?wd=你好&callback=cb,且req.headers.host是localhost:8080,那么new URL(req.url, 'http://localhost:8080')会生成一个表示http://localhost:8080/say?wd=你好&callback=cb的 URL 对象。
url.searchParams是一个URLSearchParams对象,用于操作 URL 中的查询字符串(即?后面的部分)。.get('wd')方法用于获取查询参数中键为wd的值。- 例如,如果 URL 是
/say?wd=hello,那么wd的值就是"hello"。 - 如果没有
wd参数,则返回null。
4. JSONP 的局限性
-
优点:简单,兼容老浏览器
-
缺点:
- 只能发
GET请求(因为<script>只能 GET) - 不安全,容易被黑客攻击(如果 callback 被恶意注入)
- 需要后端配合,后端的输出的方式要加
padding
- 只能发
四、现代解决方案:CORS——官方颁发的“通行证”
随着 Web 发展,W3C 推出了 CORS(Cross-Origin Resource Sharing) ,也就是“跨域资源共享”。
它的核心思想是:让服务器主动告诉浏览器:“我允许你跨域访问我。”
1. 工作原理
只要后端在响应中加上这行头:
Access-Control-Allow-Origin: http://localhost:5173
或者允许所有来源:
Access-Control-Allow-Origin: *
浏览器看到这个头,就知道:“哦,服务器允许,那我可以把数据给你了。”
2. CORS 的两种请求:简单请求 vs 复杂请求
CORS 并不是一刀切的。它根据请求的“复杂程度”,分为两类:
1. 简单请求
满足以下所有条件,就是“简单请求”:
-
方法:
GET、POST、HEAD -
Content-Type:
text/plainmultipart/form-dataapplication/x-www-form-urlencoded
-
不能自定义其他特殊头(如
Authorization、X-Token)
👉 这种请求,浏览器直接发,不打招呼。
示例:GET 请求
fetch('http://localhost:8000/api/test') // GET 请求
后端只需设置:
res.setHeader('Access-Control-Allow-Origin', '*')
res.end(JSON.stringify({ msg: '跨域成功!' }))
浏览器收到响应后,发现有 Access-Control-Allow-Origin 头,就放行数据。
2. 复杂请求(Preflight Request)
只要不符合“简单请求”的条件,就是“复杂请求”。
比如:
- 使用
PUT、PATCH、DELETE方法 Content-Type是application/json- 添加了自定义请求头(如
Authorization: Bearer xxx)
👉 这种请求,浏览器会先发一个预检请求(OPTIONS) ,问服务器:“我待会要这么干,你同不同意?”
只有服务器说“同意”,浏览器才会发送真正的请求。
实例:以 PATCH 为例
1. 前端代码(发起复杂请求)
fetch('http://localhost:8000/api/test', {
method: 'PATCH', // 不是 GET/POST,属于复杂请求
})
.then(res => res.json())
.then(data => console.log(data))
2. 浏览器自动行为:先发 OPTIONS 预检
浏览器会自动先发送一个 OPTIONS 请求:
OPTIONS /api/test
Origin: http://127.0.0.1:5500
Access-Control-Request-Method: PATCH
3. 后端必须响应预检
if (req.method === 'OPTIONS') {
// 设置允许的来源
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
// 设置允许的方法
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
// 可选:允许的头
// res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.writeHead(200) // 返回 200,表示“同意”
res.end()
return
}
4. 浏览器收到预检响应后,才发真正的 PATCH 请求
PATCH /api/test
Origin: http://127.0.0.1:5500
5. 后端处理真正请求
if (req.url === '/api/test' && req.method === 'PATCH') {
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ msg: '跨域成功!!!' }))
}
6.完整后端代码示例
const http = require('http')
const server = http.createServer((req, res) => {
// 所有请求都是跨域请求,所有响应都带上 CORS 头
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500')
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
// 预检请求(OPTIONS)
if (req.method === 'OPTIONS') {
res.writeHead(200)
res.end()
return
}
// 真正的请求
if (req.url === '/api/test' && req.method === 'PATCH') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ msg: '跨域成功!!!' }))
} else {
res.writeHead(404)
res.end('Not Found')
}
})
server.listen(8000, () => {
console.log('CORS Server running at http://localhost:8000')
})
五、CORS vs JSONP:时代的选择
| 对比项 | CORS | JSONP |
|---|---|---|
| 支持方法 | 所有(GET/POST/PUT/DELETE) | 仅 GET |
| 安全性 | 高(标准头控制) | 低(易 XSS) |
| 错误处理 | 清晰(可捕获网络错误) | 困难(script load error) |
| 现代性 | ✅ 主流方案 | ❌ 仅兼容老浏览器 |
| 使用方式 | 原生 fetch / axios | 手动创建 script 标签 |
💡 结论:除非要兼容 IE8/9,否则一律使用 CORS。
六、总结:跨域的本质是信任问题
跨域问题的本质,不是技术难题,而是安全与信任的平衡。
- 浏览器出于安全,默认禁止跨域请求(防止恶意网站窃取你的银行数据)。
- 但现代应用又需要跨域通信。
- 所以我们通过 JSONP(老方法) 或 CORS(新标准) 来建立“信任链”。
🔑 记住一句话:
跨域不是“不能通信”,而是“需要双方同意才能通信”。