跨域通信机制详解

454 阅读6分钟

引言

在 Web 开发中,跨域问题是开发者频繁遇到的拦路虎。浏览器出于安全考虑,通过同源策略限制了不同源之间的资源访问,这为前后端分离的架构带来挑战。本文将系统性解析跨域问题的原理、传统解决方案及现代标准的实现细节,并提供生产环境中的最佳实践。


一、同源策略的底层原理

1.1 同源策略的定义

同源策略是浏览器中最重要的安全机制之一,用于限制不同源之间的交互,防止恶意网站窃取用户数据或发起攻击。它确保了用户的敏感信息(如Cookie、LocalStorage等)不会被其他源的脚本非法访问。 同源策略要求以下三要素完全一致,否则即被视为跨域:

b39bba69d8f6f38a4ac170d1e6af9520.png

当前页面url被请求页面是否跨域原因
https://www.baidu.com/https://www.baidu.com/news同源(协议、域名、端口相同)
http://localhost:3000http://192.168.3.1:3000跨域域名不同
http://localhost:3000http://localhost:5000跨域端口不同
https://a.comhttp://a.com跨域协议不同
https://www.test.comhttps://www.baidu.com跨域主域不同(test/baidu)
https://a.test.comhttps://b.test.com跨域子域不同(a/b)

1.2 同源策略到底拦了什么?

场景能否访问备注
AJAX / Fetch 请求需 CORS 预检或代理
Cookie / LocalStorage / IndexedDB按源隔离
DOM 访问拿不到子页面 document
图片、CSS、JS 资源标签天然放行,但拿不到内容
WebSocket协议层面无同源限制

可以用一句话总结:同源策略破坏的是“响应数据”,不破坏“请求本身”。 后端服务器依旧能收到请求,只是浏览器把数据扣下了。


二、跨域解决方案

2.1 JSONP —— 传统解决方案

JSONP 利用 <script> 标签不受同源策略影响的特性,实现跨域请求。
前端动态创建 <script> 标签,src指向目标接口,并附加回调函数名参数(如callback=handleResponse)传递给后端来请求资源。前端提前定义好以参数为名的函数体,来获取响应数据。后端将该参数处理成函数调用的写法并传入要返回的数据,例如:handleResponse({data: 'Hello'}) 。回调函数执行,解析响应数据。
示例代码(前端):

function handleData(res) {
  console.log(res);
}
const script = document.createElement('script');
script.src = 'https://api.xxx.com/data?callback=handleData';
document.body.appendChild(script);

缺陷:

  • 仅支持 GET 请求:无法处理 POST/PUT 等方法,且参数需附加在 URL 中。
  • 依赖后端支持:后端需预设对 callback 参数的处理逻辑。

2.2 CORS(官方标准,生产首选)

CORS 全称是“跨域资源共享”。它允许浏览器向不同源的服务器,发出 XMLHttpRequest 请求。 CORS 通过预检请求(Preflight) 和响应头配置,实现安全的跨域通信。
后端设置响应头,告诉浏览器“我允许这个网站访问我的资源”。

响应头作用
Access-Control-Allow-Origin允许访问的源(可设为具体域名或 * ,但 * 不兼容其他安全头)
Access-Control-Allow-Methods允许的 HTTP 方法(如GET, POST, PUT)
Access-Control-Allow-Headers允许自定义的请求头
Access-Control-Allow-Credentials是否允许携带凭证(Cookie等,默认false)

有一些请求对服务器有着特殊的要求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json。非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求 ( OPTIONS 请求方法) ,称为 “预检”请求。其作用在于,确认当前网页所在的域名是否在服务器的许可名单中,明确可使用的 HTTP 请求方法和头信息字段。只有在这个请求返回成功的情况下,浏览器才会发出正式的请求。
后端响应示例:

app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://client.example.com'); // 允许的源
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true'); // 允许携带凭证

  if (req.method === 'OPTIONS') {
    res.sendStatus(200); // 预检请求直接返回成功
  } else {
    next();
  }
});

2.3 Nginx 反向代理

Nginx 实现原理类似于 Node 中间件代理,需要搭建一个中转 nginx 服务器,用于转发请求。即利用 nginx 反向代理功能,将请求转发到后端服务器,后端服务器收到请求后,将响应返回给 nginx,nginx 再将响应返回给前端浏览器。它是最简单的跨域方式。只需要修改 nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session ,不需要修改任何代码,并且不会影响服务器性能。


2.4 WebSocket —— 实时通信

WebSocket 是一种基于 TCP 的双向通信协议,允许客户端和服务器之间进行实时、全双工通信。 首先启动一个 HTTP 服务器,等待客户端发起连接请求。当客户端请求 WebSocket 连接时,服务器通过 HTTP 101状态码切换协议,升级为 WebSocket。一旦协议升级成功,服务器和客户端就可以通过 WebSocket 进行通信。双方通过特定格式的数据帧进行消息的发送与接收。


2.5. postMessage(iframe 场景)

当一个页面通过 iframe 标签内嵌了一个二级页面,且一级页面和二级页面要进行通信时,默认是跨域的。利用 window.postMessage() 方法实现跨域通信postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

otherWindow.postMessage(message, targetOrigin,[transfer]);
  • message: 将要发送到其他 window 的数据。
  • targetOrigin: 通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串*(表示无限制)或者一个 URL。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
  • transfer(可选):是一串和 message 同时传递的 Transferable 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

2.6 document.domain

该方法只适合子域相同,主域不同的情况,在两个页面都设置 document.domain='子域'。 比如 a.test.comb.test.com 适用于该方式,只需要给页面添加 document.domain ='test.com',表示二级域名都相同就可以实现跨域。 两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。
注意:自Chrome 101版本起,该功能已被废弃,设置操作将导致属性变为可读属性且不再生效。