深入理解跨域:什么是同源策略,JSONP->CORS

173 阅读8分钟

在现代前端开发中,尤其是使用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代码,
  • 不能通过 fetchXMLHttpRequest 直接请求 http://localhost:8080 的接口数据。

否则就会触发跨域错误。

跨域到底发生了什么?

很多人误解:“跨域请求被服务器拒绝了”。

错!真相是:

  1. 请求确实发出去了,并且服务器也收到了。
  2. 服务器正常处理并返回了响应。
  3. 浏览器在收到响应后,发现响应头没有允许跨域的标识,于是主动拦截了这个响应,不让你的JavaScript代码读取它。
  4. 控制台报错: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)

满足以下条件的请求称为“简单请求”:

  • 方法是:GETPOSTHEAD
  • 请求头只包含:AcceptAccept-LanguageContent-LanguageContent-Type(仅限 application/x-www-form-urlencodedmultipart/form-datatext/plain

后端只需加一个响应头:

Access-Control-Allow-Origin: http://localhost:5173

或者允许所有来源(不推荐生产环境使用):

Access-Control-Allow-Origin: *

前端 fetch 就能正常拿到响应!


复杂请求(Preflight Request)

如果请求是:

  • 方法为:PUTDELETEPATCH
  • 或者 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'
}));


七、总结:一张表搞懂核心区别

对比项JSONPCORS
支持方法仅 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 错误时,不再慌张!