跨域(Cross-Origin)是 Web 安全中一个核心概念,源于浏览器的同源策略。理解跨域问题,首先要理解“源”。
1. 什么是“源”?
“源”由三部分组成:协议(Protocol)、域名(Host)、端口(Port)。
只有当两个 URL 的协议、域名、端口完全相同时,才属于同源。
示例:
https://www.example.com/page1
和https://www.example.com/page2
:同源(协议、域名、端口相同)https://www.example.com
和http://www.example.com
:不同源(协议不同,https vs http)https://www.example.com
和https://api.example.com
:不同源(域名不同,www vs api)https://www.example.com
和https://www.example.com:8080
:不同源(端口不同,默认443 vs 8080)https://www.example.com
和https://sub.www.example.com
:不同源(域名不同)
2. 什么是跨域?
跨域是指: 当一个网页(运行在一个“源”下)的 JavaScript 代码尝试向与该网页不同源的服务器发起网络请求(如 AJAX、Fetch API)时,就会发生跨域。
为什么浏览器要阻止跨域请求? 主要是出于安全考虑(同源策略的核心目的):
防止恶意网站窃取数据: 阻止恶意网站 A 上的脚本偷偷读取你登录在正规网站 B 上的敏感数据(如银行账户、邮件内容)。
防止 CSRF 攻击: 增加实施跨站请求伪造攻击的难度(虽然同源策略本身不是完美的 CSRF 防御机制)。
隔离潜在恶意文档: 限制不同源文档之间的交互,防止恶意脚本篡改或读取其他文档的内容。
注:跨域请求通常都会发送到后端服务器,服务器也通常会处理请求并返回数据,但浏览器会拦截响应,不交给前端 JavaScript。
3. 哪些操作会受到同源策略限制?
XMLHttpRequest (AJAX) / Fetch API: 最常见的受限操作。浏览器默认阻止 JS 发起的跨域 HTTP 请求。
Web Fonts (CSS 中的
@font-face
): 部分浏览器会限制跨域字体加载(通常可通过 CORS 解决)。WebGL 纹理: 加载跨域图片作为纹理可能受限。
Canvas 的
drawImage()
: 将跨域图片绘制到 Canvas 上会“污染” Canvas,导致无法读取其像素数据(除非图片服务器允许且设置了 CORS)。<iframe>
中的 DOM 访问: 父页面无法直接访问不同源 iframe 内的 DOM,反之亦然(可通过postMessage
或设置document.domain
在特定子域场景下通信)。
4. 如何解决跨域问题?
解决跨域问题的核心思路是让请求在浏览器眼中变成“合法”的跨域请求,或者绕过浏览器的同源策略限制。常用方法:
主要方法
CORS (跨域资源共享):
最主流、最标准、最推荐的方式。 由 W3C 标准定义。
原理: 浏览器在发起非简单请求(如带自定义头、Content-Type 为
application/json
的 POST 等)的跨域请求前,会先自动发送一个OPTIONS
方法的预检请求。服务器需要在响应头中明确声明允许哪些源、方法、头、凭证等信息。浏览器检查预检响应通过后,才会发送真正的请求。服务器在响应真正的请求时,也需要设置相关的 CORS 头(主要是Access-Control-Allow-Origin
)。关键响应头:
Access-Control-Allow-Origin: <origin> | *
: 允许访问该资源的源。*
表示允许任何源(不推荐用于携带凭证的请求)。必须设置。Access-Control-Allow-Methods: GET, POST, PUT, DELETE, ...
: 允许客户端使用的方法。Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header, ...
: 允许客户端携带的请求头。Access-Control-Allow-Credentials: true
: 允许客户端请求携带凭证(如 Cookies、HTTP 认证信息)。如果客户端设置了withCredentials: true
,则服务器必须设置此头为true
且Access-Control-Allow-Origin
不能为*
。Access-Control-Max-Age: <seconds>
: 指定预检请求的结果可以被缓存多久。
优点: 安全、灵活、标准。前端几乎无需改动(除了可能需要设置
withCredentials
)。缺点: 需要服务器端配合修改代码(设置响应头)。旧浏览器支持度稍差(IE10+)。
JSONP (JSON with Padding):
原理: 利用
<script>
标签的src
属性不受同源策略限制的特性。客户端动态创建一个<script>
标签,其src
指向目标 API 的 URL 并附加一个回调函数名(如callback=handleResponse
)。服务器收到请求后,将数据包裹在这个回调函数调用中返回(如handleResponse({"data": "value"});
)。浏览器加载并执行该脚本,就会调用客户端定义好的回调函数,从而获取到数据。优点: 兼容性极好(支持老式浏览器)。
缺点:
只支持 GET 请求。
安全性差:服务器可能返回恶意代码,客户端必须信任服务器。难以处理错误。
缺乏标准化的错误处理机制。
本质上不是真正的 AJAX。
替代方案(绕过或代理)
代理服务器:
原理: 由于同源策略是浏览器的限制,服务器之间通信没有此限制。客户端先请求自己同源的服务器(代理服务器),该服务器再将请求转发到目标跨域服务器,获取数据后再返回给客户端。
实现方式:
Nginx 反向代理: 配置 Nginx 将匹配特定路径(如
/api/
)的请求转发到真实的后端 API 地址。Node.js 中间件代理: 在开发环境中(如 Webpack Dev Server、Vite Dev Server)配置代理选项。生产环境可用 Express、Koa 等框架编写代理中间件。
后端应用代理: 在你的后端应用(如 Java Spring Boot, Python Django/Flask, PHP Laravel)中添加一个路由来处理代理请求。
优点: 客户端代码完全不需要考虑跨域,和访问同源 API 一样。服务器端控制灵活。
缺点: 增加了一层网络跳转,可能略微影响性能。需要部署和配置额外的代理逻辑。
WebSocket:
原理: WebSocket 协议 (
ws://
,wss://
) 本身允许跨域连接。一旦建立 WebSocket 连接,客户端和服务器就可以自由地进行双向通信,不受同源策略对 HTTP 请求的限制。适用场景: 实时双向通信(聊天室、实时数据推送、在线游戏)。
优点: 真正的双向通信,高效。
缺点: 不能替代普通的 HTTP API 调用。需要服务器支持 WebSocket。协议和 API 与 HTTP 不同。
特殊场景方法
document.domain
(仅限主域相同):原理: 如果两个页面属于同一个基础域名(如
a.example.com
和b.example.com
),且都显式设置document.domain = 'example.com';
,那么浏览器会认为它们同源,允许相互访问 DOM。缺点: 仅适用于具有相同父域的子域之间。现代浏览器中限制较多(如端口重置为 null)。不推荐用于解决 API 跨域请求,主要用于 iframe 通信。
window.postMessage
:原理: 提供了一种安全的、跨源的文档间通信机制。一个窗口(或 iframe)可以向另一个窗口发送消息(字符串或可序列化对象),无论它们是否同源。接收方通过监听
message
事件来获取数据。适用场景: 不同源的
iframe
、window.open()
打开的窗口、window.opener
等之间的通信。优点: 安全可控(可以指定目标源)。
缺点: 主要用于窗口/框架间通信,不直接用于解决客户端 JS 到服务器的 API 跨域请求。需要双方页面配合编写消息处理逻辑。
总结与建议
首选 CORS: 对于现代 Web API 开发,CORS 是解决跨域问题的标准、安全和推荐方式。确保后端服务器正确配置 CORS 响应头。
开发环境用代理: 在本地开发时,利用 Webpack/Vite 等构建工具提供的代理功能非常方便,避免后端频繁修改 CORS 配置。
生产环境考虑:
如果 API 服务是你控制的,务必配置好 CORS(精确指定
Access-Control-Allow-Origin
,避免用*
,尤其是需要凭证时)。如果 API 服务不受你控制且不支持 CORS,代理服务器是可靠的选择(Nginx 或自己的后端应用做代理)。
慎用 JSONP: 除非必须支持非常古老的浏览器且目标 API 支持 JSONP,否则不建议使用。安全性差,功能有限。
按需选择其他方案:
WebSocket
用于实时双向通信,postMessage
用于页面间通信,document.domain
在特定子域场景下可用于 DOM 访问(但少用)。
理解跨域及其解决方案是前端和全栈开发者的必备知识。选择哪种方法取决于你的具体需求、控制权(是否能修改服务器配置)、目标用户浏览器支持情况以及安全性要求。