跨域:一个让无数前端开发者头疼的概念,彻底讲透它
如果你曾经在浏览器控制台看到过红色的
Access-Control-Allow-Origin报错,那么你已经被“跨域”折磨过了。这篇文章,我们从零开始,把跨域的前因后果、底层原理、解决方案,一次性梳理清楚。
一、从一个场景说起
想象一下这个场景:
你正在浏览器里访问 www.example.com 这个网站,网站里有一段 JavaScript 代码,想要去请求 test.example.com 上的数据。你觉得这段代码能成功拿到数据吗?
答案是:不能。浏览器会在控制台给你报一个红色的错误,大意是“跨域请求被拦截了”。
可问题是:
- 你在地址栏直接输入
test.example.com,能正常打开。 - 你用
curl命令在终端里请求test.example.com,也能拿到数据。 - 为什么偏偏 JavaScript 代码去请求就不行?
这就是跨域最让人困惑的地方——同样的请求,换个方式就能成功,用 JS 发就被拦截。
要理解这个问题,我们必须先搞懂一个概念:源。
二、什么是“源”?
“源”(Origin)是浏览器给每个网站贴的一个“门牌号”,由三个部分拼接而成:
| 组成部分 | 含义 | 举例 |
|---|---|---|
| 协议(Protocol) | 通信方式 | http:// 或 https:// |
| 域名(Domain) | 网站地址 | www.example.com |
| 端口(Port) | 入口编号 | :80 或 :443 |
当你在浏览器里打开一个网页时,浏览器会根据这个网页的 URL,计算出它的“源”。
https://www.example.com:443
─┬─ ───┬─── ─┬─
协议 域名 端口
只有当两个 URL 的协议、域名、端口完全一致时,它们才属于同一个“源”。
看看下面这些例子:
| 页面地址 | 请求地址 | 是否同源 | 原因 |
|---|---|---|---|
http://example.com/page | http://example.com/api | ✅ 是 | 协议、域名、端口全相同 |
http://example.com/page | https://example.com/api | ❌ 否 | 协议不同(http vs https) |
http://example.com/page | http://test.example.com/api | ❌ 否 | 域名不同(子域名不同) |
http://example.com:80/page | http://example.com:8080/api | ❌ 否 | 端口不同(80 vs 8080) |
其中第二、三、四种情况,就是所谓的跨域——从一个“源”跨越到另一个“源”去请求资源。
三、为什么要有跨域限制?
这个问题可以从一个真实的安全漏洞说起。
假如没有跨域限制……
你登录了网上银行,地址是 https://bank.com。登录后,浏览器里保存了你的登录凭证(Cookie),所以后续你对 bank.com 发起的所有请求,都会自动带上这个凭证,银行服务器就知道“这是本人操作”。
好,一切正常。然后你打开一个新标签页,访问了一个恶意网站 https://evil.com。这个网站里有一段 JS 代码:
fetch('https://bank.com/transfer?to=hacker&amount=10000')
如果没有跨域限制,这段代码会带着你的 Cookie 发出请求,银行服务器以为是你在操作,一万块钱瞬间就转走了。
这就是跨站请求伪造(CSRF)。
正是因为存在这种风险,浏览器才制定了同源策略(Same-Origin Policy):只允许当前源的 JS 代码访问同源的数据,禁止访问非同源的数据。
同源策略是浏览器最核心的安全防线之一,它保护的是用户的数据安全。
四、跨域到底“拦截”了什么?
很多人以为跨域是“请求根本没发出去”,这是最大的误解。
真相是:请求发出去了,后端也处理了,甚至返回了正确的数据,但浏览器中途把数据截胡了,没交给你的 JS 代码。
整个流程是这样的:
1. 你的 JS 代码发起请求 → ✅ 请求正常发出
2. 请求到达目标服务器 → ✅ 服务器正常处理
3. 服务器返回响应数据 → ✅ 响应正常返回
4. 浏览器收到响应 → ❌ 检查源,发现跨域 → 数据被拦截
5. 你的 JS 代码 → ❌ 收到报错,拿不到数据
所以,跨域失败的本质是“响应被浏览器扣下了”,而不是“请求没发出去”。这就是为什么后端日志里能看到请求成功(状态码 200),但前端控制台却在报跨域错误——浏览器把成功返回的数据扔掉了,只给你看了一个错误提示。
五、有哪些请求会受到跨域限制?
同源策略并不是禁止所有跨域请求,它针对的主要是由 JavaScript 发起的网络请求。
❌ 受跨域限制的请求
XMLHttpRequest对象发起的请求Fetch API发起的请求WebSocket连接(受同源策略影响,但有单独的握手机制)EventSource(SSE,Server-Sent Events)
这些请求的共同特征是:由 JS 代码主动发起,且可以读取响应内容。
✅ 不受跨域限制的请求
- 在浏览器地址栏直接输入 URL 访问
<img src="跨域地址">加载图片<script src="跨域地址">加载脚本<link rel="stylesheet" href="跨域地址">加载样式<video>、<audio>、<iframe>等标签加载资源- 表单提交(
<form action="跨域地址">,但无法读取响应)
这些请求的共同特征是:要么是用户主动触发的(地址栏访问),要么是浏览器标签自动加载的(图片、脚本),且 JS 代码无法直接读取其响应内容。
注意:
<script>标签加载跨域 JS 文件虽然不受同源策略限制,但浏览器仍会阻止跨域脚本读取你的 DOM 或 Cookie。另外,现代浏览器对<script>标签加载的跨域资源还增加了 SRI(子资源完整性)等安全校验机制。
对于 <script> 标签加载 JSONP 接口的场景,虽然请求本身不受同源策略限制,但服务器返回的是一段可执行的 JS 代码,而不是纯 JSON 数据——这和正常的 API 请求有本质区别,后面会在 CORS 章节详细说明。
正是因为这些标签不受跨域限制,JSONP、CDN 加载等方案才得以实现。同时也说明了一个关键点:跨域限制不是网络层面的拦截,而是浏览器层面的安全策略。
六、如何解决跨域问题?
理解了跨域的来龙去脉,解决它就有思路了。核心方向无非两个:要么让浏览器放行,要么让浏览器根本意识不到在跨域。
方案一:CORS(跨域资源共享)
CORS 是 W3C 制定的官方跨域解决方案。它的核心思想是:后端主动告诉浏览器“这个来源可以访问我”。
后端在响应头里加上这个字段:
Access-Control-Allow-Origin: https://www.example.com
浏览器收到响应后,检查这个头:如果当前页面的源在允许列表里,就放行数据;否则拦截。
如果后端的 API 需要开放给所有来源,可以设置为:
Access-Control-Allow-Origin: *
但生产环境不建议这么做,会有安全风险。
CORS 的两种请求
CORS 把请求分为两类:
1. 简单请求
满足以下条件的就是简单请求:
- 方法为 GET、POST、HEAD 之一
- 请求头只有 Accept、Accept-Language、Content-Language、Content-Type 等常规字段
- Content-Type 只能是
application/x-www-form-urlencoded、multipart/form-data、text/plain之一
简单请求的流程很简单:浏览器直接发出请求,后端返回 Access-Control-Allow-Origin 头,浏览器据此决定是否放行。
2. 非简单请求
不符合上面条件的就是非简单请求,比如:
- 方法为 PUT、DELETE、PATCH
- Content-Type 是
application/json - 带了自定义请求头(如
X-Token)
对于非简单请求,浏览器会先发一个 OPTIONS 预检请求,询问后端:“我打算发一个跨域请求,你允许吗?”后端返回允许的规则后,浏览器才发出真正的请求。
整个过程是这样的:
浏览器:OPTIONS /api(预检)
后端: 响应 200,带上允许的方法和头
浏览器:GET /api(正式请求)
后端: 响应数据,带上 Access-Control-Allow-Origin
浏览器:✅ 放行,交给 JS
如果你在浏览器开发者工具的 Network 面板里看到两个相同的请求,其中一个方法是 OPTIONS,那就是预检请求。
对于预检请求,后端需要明确返回允许的 HTTP 方法(Access-Control-Allow-Methods)和允许的请求头(Access-Control-Allow-Headers),否则正式请求会被浏览器拦截。
方案二:JSONP
JSONP 是 CORS 出现之前,业界普遍使用的“曲线救国”方案。
它的原理很简单:既然 <script> 标签不受跨域限制,那我就用动态创建 <script> 标签的方式来请求数据。
function handleData(data) {
console.log(data);
}
const script = document.createElement('script');
script.src = 'https://test.example.com/api?callback=handleData';
document.body.appendChild(script);
后端返回的响应像这样:
handleData({ name: '张三', age: 18 })
浏览器加载这个脚本后,自动执行 handleData 函数,数据就传进来了。
但 JSONP 的局限性很明显:
- 只支持 GET 请求
- 需要后端配合改造
- 存在安全风险(XSS 攻击面增加)
所以现在基本被 CORS 取代,仅在某些老旧系统中还能见到。
方案三:Nginx 反向代理
这是最优雅、最彻底的跨域解决方案,也是你之前配置的方案。
原理很简单:让浏览器压根意识不到自己在跨域。
你在 Nginx 里配置一条路由:
location /test/ {
proxy_pass http://test.example.com/;
}
然后前端请求的是:
https://www.example.com/test/api
浏览器一看:www.example.com 和当前页面同源,放行。至于 Nginx 内部怎么把请求转发给 test.example.com,那是服务器之间的事,浏览器看不到也管不着。
核心优势:
- 前端代码不需要任何改动
- 后端不需要配置 CORS
- 安全,因为跨域请求根本没有从浏览器发出
- 还可以顺带做负载均衡、缓存、SSL 终结等
方案四:WebSocket
WebSocket 不存在跨域限制,因为它不是基于 HTTP 的请求-响应模型,而是通过独立的握手协议建立长连接。如果你用 WebSocket 通信,天然就没有跨域问题。
方案五:postMessage + iframe
在特定场景下(比如嵌入第三方页面),可以用 window.postMessage() 配合 iframe 实现跨域通信。这种方式主要用于不同源的页面之间传递消息,而不是用于 API 请求,应用场景相对有限。
七、各方案对比与适用场景
为了帮你快速做决策,我把五种方案的核心差异整理成一张表:
| 方案 | 原理 | 需要改前端? | 需要改后端? | 适用场景 |
|---|---|---|---|---|
| CORS | 后端告诉浏览器放行 | ❌ 否 | ✅ 是 | 你控制后端 API,或需要开放 API 给第三方 |
| JSONP | <script> 标签绕过限制 | ✅ 是 | ✅ 是 | 老旧系统、仅需 GET 请求,几乎已淘汰 |
| Nginx 反向代理 | 同源请求,服务器端转发 | ❌ 否 | ❌ 否 | 首选方案,尤其是前后端在同一域名下时 |
| WebSocket | 协议本身无跨域限制 | ✅ 是 | ✅ 是 | 实时通信、双向交互场景 |
| postMessage + iframe | 页面间消息传递 | ✅ 是 | ❌ 否 | 嵌入第三方页面、跨域页面通信 |
做选择的建议:
- 如果前后端都能部署在同一个域名下 → Nginx 反向代理,最简单干净
- 如果后端是开放 API,需要给多个前端应用使用 → CORS,让后端配一次就行
- 如果需要实时双向通信 → WebSocket
- JSONP 和 postMessage 方案在 2026 年的今天已经用得很少了,了解一下原理即可,实际项目中优先考虑前三种
八、总结:一张图看懂跨域
┌─────────────────────────────────┐
│ 跨域问题全貌 │
├─────────────────────────────────┤
│ │
│ 为什么会有跨域? │
│ → 浏览器的同源策略,为了保护用户数据安全 │
│ │
│ 什么是跨域? │
│ → 协议/域名/端口不同,就是跨域 │
│ │
│ 跨域拦截了什么? │
│ → JS 发起的网络请求(Ajax、Fetch) │
│ → 不拦截:地址栏访问、标签加载、表单提交、服务器间请求 │
│ │
│ 跨域是怎么拦截的? │
│ → 请求发出去了,数据也返回了,浏览器在中途把数据扣下了 │
│ │
│ 怎么解决跨域? │
│ ├── CORS:后端告诉浏览器“放行” │
│ ├── JSONP:用 script 标签绕过(已淘汰) │
│ ├── Nginx 反向代理:让浏览器不觉得是在跨域 ★推荐 │
│ ├── WebSocket:协议本身无跨域限制 │
│ └── postMessage:页面间跨域通信 │
│ │
└──────────────────────────────────┘
最后的话
跨域之所以让人困惑,是因为它把“网络请求”和“浏览器策略”这两件事混在了一起。从网络层面看,请求畅通无阻;从浏览器层面看,数据被无情拦截。
理解了这两层的区别,跨域就不再神秘了。
回到你的场景:用 Nginx 把 www.example.com/test 代理到 test.example.com,这是最干净利落的方案——不需要改代码,不需要配 CORS,浏览器全程没离开过同源。不管后端是 HTTP 还是 HTTPS,不管有多少个子域名要转发,一套 Nginx 配置全部搞定。
如果你在实际部署中遇到具体的报错,欢迎随时来问我。