一、跨域的根本原因:同源策略
同源策略 是浏览器最核心的安全基石。它限制了从一个源 加载的文档或脚本如何与另一个源的资源进行交互。
-
什么是"同源"? 协议、域名、端口三者必须完全相同。
- 同源:
https://www.example.com/page1与https://www.example.com/page2 - 不同源:
https://www.example.com与http://www.example.com(协议不同) - 不同源:
https://www.example.com与https://api.example.com(域名不同) - 不同源:
https://www.example.com与https://www.example.com:8080(端口不同)
- 同源:
-
限制了什么?
- DOM 访问:无法用 JS 读取不同源页面的 DOM、Cookie、LocalStorage 等。
- Ajax 请求 :默认情况下,无法用
XMLHttpRequest或Fetch API向不同源的接口发送请求。 - 注意 :链接
<a>、<img>、<script>、<link>等标签的src或href属性可以 加载跨域资源,这是跨域资源共享实现的基础。
"跨域"问题的本质是:浏览器限制了脚本发起的跨域 HTTP 请求的响应数据被 JS 代码读取,但请求实际已发出并到达了服务器。
二、解决方案
1. CORS(跨域资源共享)------ 现代标准解决方案
这是 W3C 标准,由服务端通过设置 HTTP 响应头来告诉浏览器允许哪些源、方法、头部进行跨域访问。
-
简单请求 :满足特定条件(如方法为 GET/POST/HEAD,Content-Type 为
application/x-www-form-urlencoded,multipart/form-data,text/plain)。- 浏览器直接发出请求,并在请求头中自动携带
Origin。 - 服务器响应时,必须在响应头中包含
Access-Control-Allow-Origin: *或Access-Control-Allow-Origin: https://your-site.com。
- 浏览器直接发出请求,并在请求头中自动携带
-
预检请求 :不满足简单请求条件(如 Content-Type 为
application/json,或使用了 PUT/DELETE 方法等)。-
浏览器会先自动发送一个 OPTIONS 方法的"预检请求"。
-
服务器必须响应此预检请求,并返回以下关键头部:
Access-Control-Allow-Origin: 允许的源Access-Control-Allow-Methods: 允许的 HTTP 方法Access-Control-Allow-Headers: 允许的自定义请求头
-
预检通过后,浏览器才会发送真正的请求。
-
-
附带身份凭证的请求:如果请求需要携带 Cookie 或 HTTP 认证信息。
- 前端:
fetch(url, { credentials: 'include' })或xhr.withCredentials = true - 服务器:响应头必须包含
Access-Control-Allow-Credentials: true,并且Access-Control-Allow-Origin不能为*,必须是明确的源。
- 前端:
核心:CORS 的关键在于服务器端的配置。
2. JSONP(JSON with Padding)------ 历史方案,用于 GET 请求
利用 <script>标签没有跨域限制的特性来实现。
-
原理:
- 前端定义一个全局回调函数,例如
function handleResponse(data) { ... }。 - 动态创建一个
<script>标签,其src指向目标接口,并携带回调函数名作为参数,如https://api.other.com/data?callback=handleResponse。 - 服务器接收到请求后,将数据作为参数,包裹在回调函数调用中返回,如
handleResponse({"data": 123})。 - 浏览器下载并执行此脚本,相当于调用了前端的
handleResponse函数,从而拿到了数据。
- 前端定义一个全局回调函数,例如
-
缺点:
- 只支持 GET 请求。
- 安全性差,容易受到 XSS 攻击。
- 错误处理机制不完善。
3. 代理服务器(Proxy)------ 开发环境常用
在前后端分离的开发中,为了解决本地开发时的跨域问题,常用代理方案。
-
原理:由于同源策略是浏览器的限制,服务器之间通信没有此限制。可以设置一个同源的代理服务器,前端将所有请求发给这个代理,由代理转发给目标服务器,再将响应返回给前端。
-
实现:
- 开发环境 :使用 Webpack DevServer 的
proxy配置、Vite 的server.proxy配置,或 Nginx 反向代理。 - 生产环境 :通常用 Nginx 反向代理,将
/api路径的请求代理到真正的后端服务器。
- 开发环境 :使用 Webpack DevServer 的
4. 基于 iframe 的方案(已过时,了解即可)
主要是利用 document.domain降级、window.postMessage或 window.name等技巧在不同源的 iframe 之间传递消息。这些方案较为复杂,安全性有挑战,在现代 CORS 成为标准后已很少使用。
- document.domain :仅适用于主域相同、子域不同的场景(如
a.example.com和b.example.com),可同时设置document.domain = 'example.com'来实现同源。 - postMessage :HTML5 提供的安全跨源通信方法。一个窗口/iframe 可以向另一个窗口发送消息,目标窗口监听
message事件来接收。
总结与选择
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| CORS | 服务端设置响应头 | W3C标准,功能强大,支持所有HTTP方法,安全性好 | 需要服务端配合修改 | 生产环境首选,前后端分离的标准方案 |
| JSONP | <script>标签 |
兼容性极好,支持老浏览器 | 仅GET,安全性差,错误处理难 | 需要支持老旧浏览器,且接口仅支持GET的场景(越来越少) |
| 代理 | 服务端转发请求 | 前端无需修改代码,开发环境无缝切换 | 生产环境需部署代理服务器,增加架构复杂性 | 开发环境主流方案,或生产环境用于隐藏真实后端地址、负载均衡 |
| iframe | 窗口间消息传递 | 可实现特定复杂场景的跨域通信 | 实现复杂,安全性需谨慎处理,主方案过时 | 已逐渐被CORS和postMessage取代,特殊遗留系统兼容 |
现代 Web 开发的通用实践是:
- 开发阶段 :使用 Webpack/Vite 代理 或 Nginx 反向代理 来绕过跨域问题。
- 生产环境 :前后端分离部署,由后端服务正确配置 CORS 响应头 ,或通过 Nginx 反向代理 将 API 请求转发到后端,使前端看起来是在同源下请求。