前端跨域完全指南:从 JSONP 到 Nginx 反向代理,一次性彻底搞懂

作者:不会敲代码1日期:2026/6/2

前端跨域完全指南:从 JSONP 到 Nginx 反向代理,一次性彻底搞懂

同源策略是浏览器最坚实的护城河,而跨域方案就是一道道精心设计的城门。

前言

前后端分离开发早已成为标配。前端跑 localhost:5173,后端跑 localhost:3000,端口不同,跨域就来了。再加上调用第三方 API、对接合作商接口,跨域问题几乎是每个前端开发者的必修课。

这篇文章从「为什么会有跨域」出发,一次性梳理 JSONP、CORS、WebSocket、postMessage、Vite Proxy、Nginx 反向代理六种跨域方案,附带可运行的代码示例,争取做到「看一篇就够了」。


一、同源策略:跨域的根源

同源策略(Same-Origin Policy) 是浏览器最核心的安全机制。所谓「同源」,要求两个 URL 的协议、域名、端口三者完全一致。

当前页面请求目标是否同源原因
http://site.comhttp://site.com/api全相同
http://site.comhttps://site.com协议不同
http://site.comhttp://api.site.com域名不同(子域名也算)
http://site.com:3000http://site.com:4000端口不同

它到底在防什么?

设想你登录了网银,Cookie 里存着登录凭证。如果不小心点开了一个恶意网站,这个网站偷偷向网银的 API 发请求,浏览器如果放行,对方就能用你的 Cookie 执行转账操作。同源策略就是堵住这个口子:它限制非同源的网页读取资源、操作 DOM、发起受限的 HTTP 通信。

一句话定位:同源策略保护的是用户数据,防止恶意网站冒充用户访问受信站点。跨域问题本质上是「如何在安全的前提下合理突破这个限制」。


二、JSONP:上古时代的智慧热修复

JSONP(JSON with Padding)诞生于 2005 年前后,比 CORS 标准早了整整十年。它的核心思路极其巧妙:正面走不通,就走侧面。

2.1 漏洞在哪里?

同源策略有一个天然的「盲区」:<script> 标签的 src 属性不受同源策略约束。你可以随意引入任何域名的 JS 文件:

1<script src="https://cdn.example.com/library.js"></script>
2

浏览器不会拦。这本来是设计上的合理豁免(CDN 加载总得允许吧),但 JSONP 把它变成了一个数据传输通道。

2.2 完整实现

前端代码:

1function jsonp({ url, params, callback }) {
2  return new Promise((resolve, reject) => {
3    //  创建 script 标签(目前还是空壳)
4    let script = document.createElement('script')
5
6    //  在全局挂载回调函数,后端返回的代码会调用它
7    window[callback] = function(data) {
8      resolve(data)
9      document.body.removeChild(script)  // 清理现场
10    }
11
12    //   callback 参数合并进 query string
13    params = { ...params, callback }
14    let arrs = []
15    for (let key in params) {
16      arrs.push(`${key}=${params[key]}`)
17    }
18
19    //  设置 src,拼接完整 URL
20    script.src = [`${url}?${arrs.join('&')}`](https://xplanc.org/primers/document/zh/10.Bash/90.%E5%B8%AE%E5%8A%A9%E6%89%8B%E5%86%8C/EX.join.md)
21
22    //  插入 DOM,浏览器开始下载  执行
23    document.body.appendChild(script)
24  })
25}
26
27// 使用
28jsonp({
29  url: 'http://localhost:3000/say',
30  params: { wd: 'HelloWorld' },
31  callback: 'show'
32}).then(data => {
33  console.log(data)  // { id: 1, username: 'admin' }
34})
35

后端代码(Node.js):

1const http = require('http')
2
3http.createServer((req, res) => {
4  if (req.url.startsWith('/say')) {
5    const url = new URL(req.url, [`http://${req.headers.host}`](http://${req.headers.host}))
6    const callback = url.searchParams.get('callback')
7
8    // Content-Type  text/javascript,不是 application/json!
9    res.writeHead(200, { 'Content-Type': 'text/javascript' })
10
11    const data = { id: 1, username: 'admin' }
12
13    // 关键:把 JSON 数据包在函数调用里返回
14    res.end([`${callback}(${JSON.stringify(data)})`](https://xplanc.org/primers/document/zh/03.HTML/EX.HTML%20%E5%85%83%E7%B4%A0/EX.data.md))
15  } else {
16    res.writeHead(404)
17    res.end('404 Not Found')
18  }
19}).listen(3000)
20

2.3 请求全链路

1前端调用 jsonp({ callback: 'show' })
2    
3window.show = function(data) { resolve(data) }  // 挂上全局函数
4    
5script.src = "http://localhost:3000/say?wd=HelloWorld&callback=show"
6    
7appendChild  浏览器发送 GET 请求
8    
9后端解析 callback 参数,返回 JS 代码:
10    show({"id":1,"username":"admin"})
11    
12浏览器执行这段 JS  调用 window.show(data)  resolve
13    
14.then(data => console.log(data)) 拿到数据
15

「Padding」的含义就在这里:后端把 JSON 数据外面包裹了一层函数调用。没有这层壳,浏览器收到裸 JSON {"id":1} 会直接报语法错误——字符串不是合法的 JS 语句。

2.4 JSONP 的致命伤

缺陷原因
仅支持 GET<script> 标签只能发 GET,规范决定,没法改
XSS 风险加载的是外部 JS,如果对方被黑,返回的代码会直接在页面执行
阻塞渲染<script> 标签默认同步加载,执行期间页面暂停渲染
无错误处理后端挂了,script 只会静默失败,不像 XHR 有 onerror

结论:JSONP 本质是 hack,用 <script> 的设计豁免来绕过同源策略的限制。现代项目中已基本被 CORS 取代,但它作为「用巧劲解决问题」的经典案例,仍然值得理解。


三、CORS:官方的正规解法

CORS(Cross-Origin Resource Sharing,跨域资源共享)是一套基于 HTTP 头的标准化机制。和 JSONP 的「曲线救国」不同,CORS 直接告诉浏览器:「这几个域名是可信的,放行。」

3.1 核心响应头

后端在响应里加上这几个头,跨域通信就打开了:

1Access-Control-Allow-Origin: https://my-site.com    # 允许哪些域名
2Access-Control-Allow-Methods: GET, POST, PUT, DELETE # 允许哪些方法
3Access-Control-Allow-Headers: Content-Type, Authorization # 允许哪些自定义头
4Access-Control-Allow-Credentials: true               # 是否允许携带 Cookie
5

Access-Control-Allow-Origin 可以设成 * 放行全部,也可以指定白名单。现代后端框架(Express、Koa、Spring、Django 等)都有现成的 CORS 中间件,引入即用。

3.2 简单请求 vs 复杂请求

CORS 把请求分成了两档。

简单请求,浏览器直接发,不做额外检查。必须同时满足三个条件:

大部分日常的 fetch('/api/data') 就是简单请求,浏览器直接发,后端返回带 CORS 头的响应就完事。

复杂请求,会多一次预检(Preflight)。触发条件包括:

  • 用了 PUTDELETEPATCH 等非简单方法
  • 加了自定义请求头(如 AuthorizationX-Custom-Header
  • Content-Typeapplication/json

3.3 预检请求的完整过程

预检是一个先问再行动的机制。浏览器在真实请求之前,先用 OPTIONS 方法打一个探路请求:

1OPTIONS /api/users HTTP/1.1
2Origin: http://localhost:5173
3Access-Control-Request-Method: DELETE
4Access-Control-Request-Headers: Authorization
5

后端收到后,返回一组 CORS 头表明态度:

1HTTP/1.1 204 No Content
2Access-Control-Allow-Origin: http://localhost:5173
3Access-Control-Allow-Methods: GET, POST, PUT, DELETE
4Access-Control-Allow-Headers: Content-Type, Authorization
5

浏览器拿到 204 响应,逐条检查:域名在不在白名单?方法允不允许?请求头支不支持?全通过了,才发出真正的 DELETE 请求。

注意:预检是浏览器自动完成的,前端代码不需要任何特殊处理。你写好 fetch,浏览器在幕后完成两段式交互。


四、WebSocket:绕过同源的天生通行证

WebSocket 是一个独立的协议(ws:// / wss://),不归同源策略管。它天然可以跨域通信。

1// 先通过 HTTP 协议握手
2new WebSocket('ws://chat.example.com')
3
4// 服务器返回 101 状态码,协议升级
5// HTTP/1.1 101 Switching Protocols
6
7// 此后双方基于消息机制进行全双工通信
8ws.onmessage = (event) => {
9  console.log('收到消息:', event.data)
10}
11ws.send('Hello Server')
12

WebSocket 建立连接时依赖 HTTP 完成一次握手(Upgrade 请求),握手成功后协议从 HTTP 升级为 WebSocket,之后就不再走 HTTP 那一套了。由于它不是 HTTP 协议,同源策略的约束对它无效。

适用场景很明确:聊天、实时协作、游戏、金融行情推送等需要服务端主动推送的场景。如果只是普通的 RESTful 接口调用,没必要为跨域而上 WebSocket。


五、postMessage:窗口之间的秘密通道

HTML5 提供的 postMessage API,让不同源的窗口、iframe、页面之间可以互相传递消息,即使它们域名完全不同。

典型场景是第三方支付的流程:

1主站页面(shop.com)
2        postMessage 传订单详情
3第三方支付 iframe(pay.com)
4        用户完成支付
5        postMessage 回传支付结果
6主站页面收到消息,更新订单状态
7

基本用法:

1// 主站  iframe
2const iframe = document.querySelector('#paymentFrame')
3iframe.contentWindow.postMessage(
4  { orderId: '123', amount: 99 },
5  'https://pay.com'  // 明确指定目标域名,防止信息泄露
6)
7
8// iframe  主站
9window.parent.postMessage(
10  { status: 'success', orderId: '123' },
11  'https://shop.com'
12)
13
14// 接收端
15window.addEventListener('message', (event) => {
16  // 一定要验证来源!
17  if (event.origin !== 'https://shop.com') return
18  console.log('收到消息:', event.data)
19})
20

postMessage 的精髓在于 origin 校验:发送时指定目标域名,接收时验证来源域名。这保证消息不会被恶意页面截获或伪造。如果省略 origin 校验直接处理消息,相当于把「安全通道」变成了「任意门」。

需要注意的是,<iframe> 标签本身会带来性能开销(每个 iframe 是一个独立的渲染上下文),现代方案更推荐用弹窗重定向等更轻量的方式处理支付这类场景。


六、反向代理:藏在服务器背后的聪明方案

前面五种方案都是在正面解决跨域:要么让后端配合(CORS),要么利用协议的漏洞或豁免(JSONP、WebSocket、postMessage)。反向代理的思路完全不同:既然跨域是浏览器的限制,那就不要让浏览器知道它跨域了。

核心逻辑:浏览器自始至终只跟一个地址通信,代理服务器作为中间人,替浏览器去跟真正的后端交涉,再把结果带回来。

6.1 Vite 开发环境代理

本地开发时,Vue/React 项目跑在 localhost:5173,后端跑在 localhost:3000。配置文件长这样:

1// vite.config.js
2export default defineConfig({
3  server: {
4    proxy: {
5      '/api': {
6        target: 'http://localhost:3000',  // 真正后端地址
7        changeOrigin: true,               // 修改 Host 头,避免后端识别错误
8        rewrite: (path) => path.replace(/^\/api/, '')  // 去掉 /api 前缀
9      }
10    }
11  }
12})
13

请求链路:

1前端代码: fetch('/api/users')
2     浏览器补全为
3浏览器发出: http://localhost:5173/api/users
4     同源(页面的 origin 也是 localhost:5173),浏览器放行
5Vite dev server 收到,匹配到 /api 代理规则
6      Node.js(非浏览器!)向 target 发请求
7Node.js 发出: http://localhost:3000/users
8     服务器对服务器通信,没有同源策略约束
9后端返回数据
10     Vite 把响应传给浏览器
11浏览器: 拿到了数据
12

关键就在这里:Vite dev server 是一个 Node.js 进程,它用自己的 HTTP 模块去请求后端。Node.js 发 HTTP 请求时,没有同源策略的约束。 浏览器自始至终只和 localhost:5173 说过话,它完全不知道后端 localhost:3000 的存在。

6.2 Nginx 生产环境代理

生产环境的思路完全一致,只是执行者从 Vite dev server 换成了 Nginx:

1server {
2    listen 80;
3    server_name my-domain.com;
4
5    # 前端静态资源
6    location / {
7        root   /usr/share/nginx/html;
8        index  index.html;
9    }
10
11    # 反向代理 API 请求
12    location /api/ {
13        proxy_pass https://api.example.com/;   # 注意末尾的 / 会自动去掉 /api 前缀
14        proxy_set_header Host $host;
15        proxy_set_header X-Real-IP $remote_addr;
16        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
17        proxy_set_header X-Forwarded-Proto $scheme;
18    }
19}
20

proxy_pass 末尾的 / 是一个容易踩的坑:

这个行为等价于 Vite proxy 里的 rewrite,只是语法不同。

生产环境流量路径:

1用户浏览器  Nginx(80端口)  静态资源走 /  返回 index.html
2                             API 请求走 /api/  proxy_pass 转发到后端  返回数据
3

浏览器只知道 my-domain.com 一个地址,前端部署和后端 API 被 Nginx 统一收敛到了同一个域名、同一个端口下。对浏览器来说这就不是跨域,对架构师来说这实现了关注点分离。

6.3 什么时候用 CORS,什么时候用反向代理?

反向代理CORS
同一团队控制前后端和部署第三方公开 API / 合作商接口
浏览器始终同源,零感知需要接口提供方配合设置响应头
配置在运维层,前端零改动白名单灵活控制访问权限
适合部门内部、公司内部系统适合集团子公司、外部合作伙伴

两者不互斥,很多项目同时使用:内部接口走 Nginx 代理,外部第三方调 CORS。


七、六种方案一张表

方案原理适用场景局限性
JSONP利用 <script> 标签 src 不受同源限制历史遗留项目、极简 GET 请求仅 GET、XSS 风险、无错误处理
CORS后端设置 HTTP 响应头声明白名单现代 Web 应用的标准方案需要后端配合、复杂请求多一次 OPTIONS
WebSocket独立协议,不归同源策略管实时通信(聊天、游戏、行情)不适合普通 RESTful 接口
postMessageHTML5 跨窗口通信 APIiframe / 弹窗跨域通信需双方配合、必须验证 origin
Vite Proxy开发环境 Node.js 反向代理本地开发跨域调试仅开发环境
Nginx 反向代理生产环境服务器转发,浏览器无感知线上部署的统一入口需运维配置、内部使用为主

写在后面

跨域不是一个孤立的知识点,它串起了浏览器安全模型、HTTP 协议、前端工程化和部署架构。理解跨域的方案,本质上是在理解「浏览器为什么这样设计」以及「我们如何在不破坏安全的前提下让通信变得可能」。

从 JSONP 这种早期 hack,到 CORS 成为标准,再到反向代理这一运维层的优雅解法,跨域方案的演进也折射了 Web 开发生态从「能用就行」到「工程化、规范化」的成熟过程。

如果你觉得这篇文章有帮助,欢迎点赞收藏,也欢迎在评论区交流你的跨域踩坑经历 👋


前端跨域完全指南:从 JSONP 到 Nginx 反向代理,一次性彻底搞懂》 是转载文章,点击查看原文


相关推荐


详解MySQL事务(超详细版)
一条泥憨鱼2026/5/25

🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临) 🎬精选专栏:数据结构与算法,JavaSE ,苍穹外卖日记 前言: “事务(Transaction)”是数据库开发里非常重要的知识。 简单来说: 事务就是“一组操作,要么全部成功,要么全部失败”。 它主要用于: 转账 下订单 库存扣减 支付系统 多表更新 这些场景都不能只执行一半,否则数据就会出错。 一、为什么需要事务? 先看一个经典案例: 银行转账 假设: 张三账户:1000 元


OpenClaw梦境系统使用介绍
handsomestWei2026/5/3

OpenClaw梦境系统使用介绍 全文链接:OpenClaw梦境系统使用介绍 本文整理 OpenClaw 2.x / 2.5 路线上围绕 Dream Engine(梦境 / 记忆抽象系统) 的能力划分、工作流、指令与场景示例。安装方式、子命令与频道行为会随版本迭代变化,以当前环境 openclaw --help 与官方文档为准。 一、2.x 新功能概览 功能模块关键改进使用上的直接收益① Dream Engine(记忆 / 梦境系统)引入 Dream 概念:对原始 Memor


DeepSeek-V4-Pro 写代码到底行不行?我拿 GLM-5.1 跟它硬碰硬比了一轮
孟健AI编程2026/4/24

大家好,我是孟健。 DeepSeek-V4-Pro 发了,官方说代码能力大幅升级。这种话我听得多了,每次新模型发布都这么说。 但我确实好奇:V4 在写代码这件事上,到底有没有追上 GLM-5.1? GLM-5.1 是我日常写代码的主力模型,用了几个月了,它什么水平我心里有数。所以这次我不跑 benchmark,不拼跑分,就拿我实际工作中的四个场景,让两个模型正面硬刚。 四个场景:源码分析、功能实现、大文件拆分、项目架构分析。 最后再算笔账,看看成本谁更划算。 场景一:项目分析,分析 Claude


飞书机器人权限批量导入
itmanll2026/4/15

{ "scopes": { "tenant": [ "contact:contact.base:readonly", "im:app_feed_card:write", "im:biz_entity_tag_relation:read", "im:biz_entity_tag_relation:write", "im:chat", "im:chat.access_event.bot_p2p_chat:read",


云计算基础
**Cara**2026/4/7

1.数据中心 数据中心就是用稳定的电力、网络、机房环境,支撑服务器和存储,安全、可靠、不间断地跑业务、存数据。 1.简述 1.数据中心: IDC Internet Data Center(互联网数据中心) 从作用上来看,数据中心就是一个超大号的机房,里面有很多很多的服务器,专门对数据进行集中管理(存储、计算、交换) 定义:一套复杂的设施 能容纳多个服务器及通信设备 2.数据中心级别 3.数据中心选址 地理条件: 海拔高 气温低 非地震带 成本因素 电费 政策导


TRAE Friends@济南第4次活动:100+极客集结,2小时极限编程燃爆全场!
飞哥数智谈2026/3/30

飞哥数智谈,现居于济南,AI提效、AI编程实践者,AI·Spring 社群发起人,同时,担任 TRAE Friends 社区济南 Fellow,致力于AI 提效与AI编程落地,最近长期举办 openclaw 系列活动《养虾记》。 昨天(3.28),100多位 AI 爱好者带着电脑齐聚齐鲁软件园,大家现场编程、热烈讨论、共同路演,活动气氛空前热烈。 活动感受 说实话,活动的热度有点超出预期。 要知道,这次活动不是嘉宾分享会、也不是普通的交流会,而是一次要求每个人带着电脑,现场编程的路演型 Wo


51 与32 单片机LED控制详解
小郝 小郝2026/3/22

针对LED控制的不同方法,需要根据单片机架构进行区分。以下是按单片机种类分类的函数用法说明: 51系列单片机控制方法 1. 直接端口操作 // 传统51单片机(如AT89C51) #include <reg51.h> sbit LED = P1^0; // 定义P1.0引脚控制LED void main() { while(1) { LED = 0; // 点亮LED(共阳极接法) Delay_ms(500); LED =


严守AI服务器冷凝板平面度基准,光子精密3D工业相机保障散热效能
PHOSKEY2026/3/14

在AI服务器算力密度持续攀升的背景下,CPU与GPU的功耗已突破数百瓦甚至向千瓦级迈进,传统的风冷散热方式逐渐逼近物理极限,液冷散热已成为高密度部署的必然选择。冷凝板(Cold Plate)作为液冷系统的核心部件,直接与发热芯片接触,通过内部冷却液的流动将热量带出。其底面与芯片的贴合紧密程度,直接决定了接触热阻的大小,进而影响整个散热系统的效率。若冷凝板底面平面度超差,微小间隙将导致局部热点积聚,芯片结温上升,最终影响AI训练任务的稳定性与算力输出。因此,企业严格把控冷凝板的平面度,本质上是为高


STM32通过代码分析Callback回调函数的调用
可乐鸡翅好好吃2026/3/5

问题:我调用了 HAL_UART_Transmit_IT() 或 HAL_UART_Receive_IT(),这里面使能了中断,且中断回调函数被调用了,代码里完全看不到 HAL_UART_TxCpltCallback() 或 HAL_UART_RxCpltCallback() 被调用,为什么回调函数还是能执行? 真相是:回调函数确实被调用了,但被 HAL 库“藏”得很深,藏在了中断处理函数的子函数里 以下通过串口中断发送来查看回调函数的调用: 一、解释串口两种发送模式 首先解释一下


【转载】Cowork and plugins for teams across the enterprise
是魔丸啊2026/2/25

转载 btw,文中提到的pluings,官方都有github仓库: github.com/anthropics/… github.com/anthropics/… 管理员现在可以创建私有插件市场,对 plugins、connectors 和 skills 进行更好的控制。我们还为更多部门添加了新的 plugins 和 connectors。 今天,我们推出了 Cowork 和 plugins 的更新,帮助 enterprises 根据工作方式定制 Claude。Plugins 将 Cla

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读