跨域问题从青铜到王者:JSONP、CORS原理详解与实战(前端必会)

一、前言:为什么我们总在谈"跨域"?

作为一名前端开发者,你一定遇到过这样的报错:

bash 复制代码
Access to fetch at 'http://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

红字警告,请求被拦截,数据拿不到,页面空白......是不是很熟悉?

这,就是臭名昭著的 跨域问题(Cross-Origin Resource Sharing)

在前后端分离架构盛行的今天,前端跑在 localhost:3000,后端接口在 api.example.com:8080,看似只是"换个地址",实则触发了浏览器的 同源策略(Same-Origin Policy) ------这是浏览器为保障用户安全而设立的"防火墙"。

那跨域真的"跨"不过去吗?当然不是。本文将带你从 JSONP 到 CORS,深入浅出地理解跨域的本质与解决方案,让你在面试和实战中游刃有余。

二、什么是"同源"?为什么要有同源策略?

所谓"同源",指的是 协议(protocol)、域名(host)、端口(port) 三者完全相同。

比如:

URL 是否同源于 http://localhost:3000
http://localhost:3000/api ✅ 同源
https://localhost:3000/api ❌ 协议不同(https vs http)
http://127.0.0.1:3000/api ❌ 域名不同(localhost vs 127.0.0.1)
http://localhost:8080/api ❌ 端口不同

只要有一个不同,就算"跨源"。

同源策略的目的

浏览器引入同源策略,是为了防止恶意网站通过脚本读取其他网站的数据,比如:

  • 恶意页面通过 <script> 加载银行网站的接口,窃取用户信息?
  • 通过 XMLHttpRequest 直接读取你邮箱里的私密内容?

听起来吓人吧?所以浏览器规定:

只有同源的资源才能被脚本(如 AJAX、fetch)自由访问。

但现实开发中,前后端分离、微服务架构、CDN 静态资源分发......跨域是常态。于是,我们得想办法"合法跨域"。

三、远古方案:JSONP ------ 利用 <script> 的"漏洞"

<script> 为什么能跨域?

你有没有发现,下面这段代码从不会报跨域错误?

html 复制代码
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>

没错,<script><img><link> 等标签天生支持跨域加载资源。这是浏览器为了正常网页功能(如加载 CDN 资源)而留下的"后门"。

JSONP(JSON with Padding)正是利用了这一点。

JSONP 的核心思想

既然 fetchXMLHttpRequest 被 CORS 拦截,那我们就不用它们,改用 <script> 标签来"发请求"。

<script> 加载的是 JS 脚本,不是 JSON 数据。怎么办?

思路:让后端返回一段 JS 代码,调用一个前端定义好的函数,并把数据作为参数传进去。

手写一个 JSONP 实现

前端代码:

javascript 复制代码
function jsonp(url, params = {}, callbackName = 'callback') {
  return new Promise((resolve, reject) => {
    // 生成唯一函数名,防止冲突
    const fnName = 'jsonp_' + Date.now() + '_' + Math.random().toString(36).substr(2);

    // 挂载全局函数
    window[fnName] = function (data) {
      resolve(data);
      // 清理:删除 script 标签和全局函数
      document.body.removeChild(script);
      delete window[fnName];
    };

    // 构造带 callback 参数的 URL
    const queryString = new URLSearchParams({
      ...params,
      [callbackName]: fnName
    }).toString();

    const script = document.createElement('script');
    script.src = `${url}?${queryString}`;
    
    script.onerror = () => {
      reject(new Error('JSONP request failed'));
      document.body.removeChild(script);
      delete window[fnName];
    };

    document.body.appendChild(script);
  });
}

// 使用示例
jsonp('http://api.example.com/user', { id: 123 })
  .then(data => console.log('用户数据:', data))
  .catch(err => console.error('请求失败:', err));

后端返回格式(Node.js 示例)

js 复制代码
app.get('/user', (req, res) => {
  const { id, callback } = req.query;
  const userData = { id, name: '归于尽', age: 21 };

  // 返回 JS 函数调用
  res.setHeader('Content-Type', 'application/javascript');
  res.send(`${callback}(${JSON.stringify(userData)})`);
});

返回内容实际是:

js 复制代码
jsonp_123456789_abc({"id":123,"name":"归于尽","age":21})

JSONP 的局限性

  • ✅ 优点:兼容老浏览器(IE6 都能用)
  • ❌ 缺点:
    • 只支持 GET 请求
    • 需要后端配合返回函数调用
    • 安全性差(易被 XSS 攻击)
    • 错误处理不完善

所以,JSONP 已逐渐被淘汰,仅用于兼容极老项目。

四、现代主流方案:CORS(跨域资源共享)

CORS 是什么?

CORS(Cross-Origin Resource Sharing)是 W3C 制定的标准,允许服务器声明哪些外域可以访问其资源。

核心机制:通过 HTTP 响应头控制跨域权限。

只要后端在响应中带上:

http 复制代码
Access-Control-Allow-Origin: http://localhost:3000

浏览器就会放行该跨域请求。

简单请求 vs 预检请求

CORS 将请求分为两类:

✅ 简单请求

满足以下所有条件:

  1. 请求方法是:GETPOSTHEAD
  2. Content-Type 仅限:
    • text/plain
    • application/x-www-form-urlencoded
    • multipart/form-data

这类请求不会触发预检 ,浏览器直接发送请求,后端加个 Access-Control-Allow-Origin 即可。

前端代码示例:

javascript 复制代码
fetch('http://api.example.com/data', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  body: 'name=归于尽&age=21'
})
.then(res => res.json())
.then(data => console.log(data));

后端 Node.js 配置:

js 复制代码
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Methods', 'GET,POST,HEAD');
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  next();
});

🔥 复杂请求(需要预检)

只要不符合"简单请求"条件,就会触发 预检请求

浏览器先发送一个 OPTIONS 请求,询问服务器:"我能不能跨域发这个请求?"

服务器必须返回允许的配置,浏览器才会发送真正的请求。

触发预检的常见情况:

  • 使用 PUTDELETEPATCH 方法
  • 自定义请求头,如 Authorization: Bearer xxx
  • Content-Type 为 application/json(注意!)

前端示例(触发预检):

javascript 复制代码
fetch('http://api.example.com/user/123', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer xxx' // 自定义头
  },
  body: JSON.stringify({ name: '归于尽' })
})
.then(res => res.json())
.then(data => console.log(data));

后端必须响应 OPTIONS 请求:

js 复制代码
app.options('/user/*', (req, res) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.sendStatus(200); // 返回 200 表示允许
});

// 实际 PUT 接口
app.put('/user/:id', (req, res) => {
  // 也要带上 CORS 头
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.json({ success: true, data: req.body });
});

常用 CORS 响应头

Header 说明
Access-Control-Allow-Origin 允许的源,可设具体域名或 *(但 * 不支持 credentials)
Access-Control-Allow-Methods 允许的 HTTP 方法
Access-Control-Allow-Headers 允许的请求头
Access-Control-Allow-Credentials 是否允许携带 Cookie(设为 true 时,Origin 不能为 *
Access-Control-Max-Age 预检结果缓存时间(秒)

五、其他跨域方案(补充)

方案 适用场景 说明
Nginx 反向代理 开发/生产环境 将 API 请求代理到同域,彻底避免跨域
Webpack DevServer 代理 开发环境 proxy: { '/api': 'http://localhost:8080' }
PostMessage iframe 通信 页面与 iframe 间跨域传数据
WebSocket 实时通信 不受同源策略限制

推荐:开发用代理,生产用 CORS

六、面试高频问题

Q1:跨域请求到底发出去了吗?

是的。请求已经到达服务器 ,服务器也正常处理并返回了结果。但浏览器在收到响应后,检查 Access-Control-Allow-Origin 头,发现不匹配,于是拦截响应结果,不交给前端 JS

Q2:JSONP 为什么只支持 GET?

因为 <script src="..."> 本质是 GET 请求,无法设置请求体或方法。

Q3:CORS 需要前端做什么?

通常不需要。只要后端配置正确,前端 fetchaxios 可以照常使用。但若涉及 Cookie,需设置:

js 复制代码
fetch('/api', {
  credentials: 'include' // 携带 Cookie
})

同时后端必须设置:

http 复制代码
Access-Control-Allow-Origin: 具体域名(不能是 *)
Access-Control-Allow-Credentials: true

七、总结

方案 优点 缺点 推荐场景
JSONP 兼容老浏览器 仅 GET,不安全 遗留系统
CORS 标准,功能全 需后端配合 ✅ 主流方案
代理 前端无感 需部署支持 开发/特定生产环境

一句话总结:现代开发首选 CORS,开发阶段可用代理,JSONP 了解即可。

参考资料

  • MDN Web Docs: CORS
  • W3C CORS Specification
  • Express.js 官方文档
相关推荐
Dolphin_海豚23 分钟前
vapor 语法糖是如何被解析的
前端·源码·vapor
Bdygsl1 小时前
前端开发:HTML(5)—— 表单
前端·html
望获linux2 小时前
【实时Linux实战系列】实时数据流处理框架分析
linux·运维·前端·数据库·chrome·操作系统·wpf
国家不保护废物2 小时前
TailwindCSS:原子化CSS的革命,让React开发爽到飞起!🚀
前端·css·react.js
程序视点2 小时前
如何高效率使用 Cursor ?
前端·后端·cursor
前端领航者2 小时前
重学Vue3《 v-for的key属性:性能差异与最佳实践》
前端·javascript
Andy_GF2 小时前
纯血鸿蒙HarmonyOS Next 远程测试包分发
前端·ios·harmonyos
嗑药狂写9W行代码3 小时前
cesium修改源码支持4490坐标系
前端
小山不高3 小时前
react实现leaferjs编辑器之形状裁剪功能点
前端