在现代前端开发中,尤其是使用React、Vue等框架进行前后端分离开发时,跨域问题是每个开发者都必须面对的常见难题,带你一步步理解什么是跨域、为什么会有跨域、浏览器的CORS机制如何工作,以及常见的解决方案如JSONP和CORS的具体实现原理。
一、什么是“跨域”?—— 先搞清楚“同源”
我们常说的“跨域”,其实是“跨源请求”(Cross-Origin Request)的简称。要理解“跨”,就得先知道什么叫“同源”。
什么是“同源”?
“同源”指的是两个URL的以下三个部分完全相同:
协议(protocol) + 域名(domain) + 端口(port)
这三个部分合起来称为一个 “源(origin)”。
举个例子:
| URL | 是否同源? |
|---|---|
http://localhost:5173 | 原始地址 |
http://localhost:5173/page | 同源(协议+域名+端口都一样) |
https://localhost:5173 | 不同源(协议不同,http vs https) |
http://localhost:8080 | 不同源(端口不同) |
http://127.0.0.1:5173 | 不同源(虽然IP指向本地,但域名不同) |
http://www.baidu.com/api/user | 完全不同的域名 |
总结:只要协议、域名、端口任意一个不同,就是“跨源”,也就是“跨域”。
二、为什么会有跨域限制?—— 同源策略(Same-Origin Policy)
浏览器的安全机制:同源策略
浏览器为了保护用户安全,引入了 同源策略(Same-Origin Policy)。这个策略规定:
一个网页的脚本(JavaScript)只能访问与它同源的资源。
这意味着:
- 你在
http://localhost:5173的页面上运行的JS代码, - 不能通过
fetch或XMLHttpRequest直接请求http://localhost:8080的接口数据。
否则就会触发跨域错误。
跨域到底发生了什么?
很多人误解:“跨域请求被服务器拒绝了”。
错!真相是:
- 请求确实发出去了,并且服务器也收到了。
- 服务器正常处理并返回了响应。
- 但浏览器在收到响应后,发现响应头没有允许跨域的标识,于是主动拦截了这个响应,不让你的JavaScript代码读取它。
- 控制台报错:
CORS policy blocked ...
关键点:跨域是浏览器拦的,不是服务器拦的!
三、为什么要限制跨域?—— 安全考虑
设想这样一个场景:
你登录了银行网站 https://bank.com,cookie里存了你的登录凭证。
然后你打开了一个恶意网站 http://evil.com,它偷偷执行:
fetch('https://bank.com/api/transfer', {
method: 'POST',
credentials: 'include', // 携带cookie
body: JSON.stringify({ to: 'hacker', amount: 10000 })
})
如果没有同源策略,这个请求就会带着你的登录cookie发送出去,可能导致资金被转走!
所以,同源策略是为了防止恶意网站冒充用户发起请求,保护用户的数据安全。
四、如何解决跨域?—— 正确的思路
我们的目标是:
让前端能拿到跨域资源
同时不违反浏览器的安全策略(CORS)
浏览器允许哪些跨域行为?
并不是所有跨域都被禁止,以下标签天生支持跨域:
<img src="https://xxx.com/1.jpg"><script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script><link rel="stylesheet" href="https://cdn.example.com/style.css">
这些标签可以自由加载外部资源,但不能读取响应内容(比如你不能通过JS获取图片的二进制数据)。
我们可以利用这一点来绕过CORS限制。
五、经典方案:JSONP(JSON with Padding)
JSONP 的核心思想
利用 <script> 标签可以跨域加载JS文件的特性,让服务器返回一段可执行的JS代码,把数据“包裹”在一个函数调用中传回来。
实现原理
1. 前端:定义一个回调函数,并动态创建 script 标签
<script>
// 定义一个函数,等待被调用
function handleUserData(data) {
console.log("收到用户数据:", data);
document.getElementById("name").textContent = data.name;
}
// 动态创建 script 标签,发起跨域请求
const script = document.createElement("script");
script.src = "http://localhost:8080/api/user?callback=handleUserData";
document.body.appendChild(script);
</script>
2. 后端:接收 callback 参数,返回函数调用
当请求 http://localhost:8080/api/user?callback=handleUserData 时,后端不再返回纯JSON:
{ "name": "Alice", "age": 25 }
而是返回一段JS代码:
handleUserData({"name": "Alice", "age": 25});
3. 浏览器执行返回的JS
浏览器加载完这个脚本后,会自动执行:
handleUserData({"name": "Alice", "age": 25});
从而触发前端定义的函数,拿到数据。
SONP 的优点
- 简单,兼容老浏览器(IE6都支持)
- 能绕过CORS限制
JSONP 的缺点
1. 安全隐患
- XSS风险:由于JSONP的工作原理是动态创建一个
<script>标签并加载外部脚本,这使得它容易受到跨站脚本攻击(XSS)。如果攻击者能够控制回调函数名或服务器响应的内容,他们就可以注入恶意的JavaScript代码,导致用户数据泄露或其他安全问题。 - CSRF风险:JSONP请求通常会自动带上用户的认证信息(如cookies),这意味着攻击者可以通过诱导用户访问特定页面触发JSONP请求,进而利用用户的登录状态执行未授权的操作。
2. 仅支持GET请求
- JSONP只能用来发起HTTP GET请求,因为它是基于
<script>标签加载远程脚本的方式工作的。对于需要使用POST、PUT、DELETE等其他HTTP方法的应用场景来说,JSONP就显得无能为力了。
3. 错误处理困难
- 当使用JSONP时,如果服务器端出现问题或者网络错误发生时,很难捕获这些错误。传统的错误处理机制(比如try-catch块)无法应用于异步加载的
<script>标签,因此很难知道请求是否成功完成或是遇到了问题。
4. 需要后端配合
- 要使用JSONP,不仅前端需要做出相应调整,后端也需要特别设计以支持这种模式。具体而言,后端需要接受一个名为
callback的参数,并根据这个参数值包装返回的数据。这对于已经存在的API接口来说可能需要额外的工作来进行改造。
5. 不适用于所有类型的API
- 并非所有的API都适合采用JSONP的形式。特别是那些涉及敏感操作或需要高度安全性保证的服务,通常不会开放给JSONP调用,以免增加被攻击的风险。
6. 维护复杂度高
- 使用JSONP往往会导致代码变得更加复杂,尤其是在管理多个回调函数以及确保它们正确执行方面。随着项目规模的增长,维护这样的代码库可能会变得越来越困难。
总之,JSONP只是传给前端一串冷冰冰的代码,没有实际意义,所以 JSONP 现在基本只用于兼容老项目或特定场景(如嵌入广告、统计代码)。
六、现代主流方案:CORS(Cross-Origin Resource Sharing)
CORS 是什么?
CORS 是 W3C 制定的一种标准,允许服务器声明哪些外部源可以访问它的资源。
核心思想:服务器通过设置特殊的HTTP响应头,告诉浏览器:“我允许这个来源的请求”。
简单请求(Simple Request)
满足以下条件的请求称为“简单请求”:
- 方法是:
GET、POST、HEAD - 请求头只包含:
Accept、Accept-Language、Content-Language、Content-Type(仅限application/x-www-form-urlencoded、multipart/form-data、text/plain)
后端只需加一个响应头:
Access-Control-Allow-Origin: http://localhost:5173
或者允许所有来源(不推荐生产环境使用):
Access-Control-Allow-Origin: *
前端
fetch就能正常拿到响应!
复杂请求(Preflight Request)
如果请求是:
- 方法为:
PUT、DELETE、PATCH等 - 或者
Content-Type: application/json - 或者带有自定义请求头(如
Authorization: Bearer xxx)
浏览器会先发送一个 预检请求(Preflight Request),使用 OPTIONS 方法询问服务器是否允许该跨域请求。
预检请求示例:
OPTIONS /api/user HTTP/1.1
Origin: http://localhost:5173
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: Content-Type, Authorization
服务器必须返回:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400 // 缓存预检结果1天
只有预检通过,浏览器才会发送真正的请求。
后端如何开启CORS?(以Node.js为例)
// Express 示例
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "http://localhost:5173");
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") {
return res.sendStatus(204); // 预检请求直接返回204
}
next();
});
或者使用中间件:
npm install cors
const cors = require('cors');
app.use(cors({
origin: 'http://localhost:5173'
}));
七、总结:一张表搞懂核心区别
| 对比项 | JSONP | CORS |
|---|---|---|
| 支持方法 | 仅 GET | 所有方法 |
| 安全性 | 差(执行JS) | 好(标准机制) |
| 错误处理 | 困难 | 支持 try/catch |
| 后端配合 | 必须返回函数调用 | 设置响应头即可 |
| 推荐程度 | 仅兼容旧项目 | 推荐使用 |
八、面试高频问题
Q1:跨域请求,请求发出去了吗?
A:发出去了,服务器也收到了,但浏览器拦截了响应。
Q2:CORS是前端还是后端解决?
A:主要是后端设置响应头,前端无法单独解决。
Q3:JSONP为什么只能发GET?
A:因为
<script src>本质是GET请求,无法携带请求体。
Q4:如何解决PUT/DELETE跨域?
A:使用CORS,后端支持
OPTIONS预检请求。
结语
跨域是前端开发绕不开的话题。理解它的本质——浏览器的安全策略,以及掌握主流解决方案——CORS,是每个前端工程师的必备技能。
记住一句话:
“跨域不是网络问题,是浏览器的安全限制;解决之道不在前端,而在后端的响应头。”
希望这篇文章能帮你彻底搞懂跨域,下次遇到 CORS policy blocked 错误时,不再慌张!