引言
在 Web 开发中,跨域问题 是开发者频繁遇到的拦路虎。浏览器出于安全考虑,通过同源策略限制了不同源之间的资源访问,这为前后端分离的架构带来挑战。本文将系统性解析跨域问题的原理、传统解决方案及现代标准的实现细节,并提供生产环境中的最佳实践。
一、同源策略的底层原理
1.1 同源策略的定义
同源策略是浏览器中最重要的安全机制之一,用于限制不同源之间的交互,防止恶意网站窃取用户数据或发起攻击。它确保了用户的敏感信息(如Cookie、LocalStorage等)不会被其他源的脚本非法访问。 同源策略要求以下三要素完全一致,否则即被视为跨域:

当前页面url | 被请求页面 | 是否跨域 | 原因 |
---|---|---|---|
https://www.baidu.com/ |
https://www.baidu.com/news |
否 | 同源(协议、域名、端口相同) |
http://localhost:3000 |
http://192.168.3.1:3000 |
跨域 | 域名不同 |
http://localhost:3000 |
http://localhost:5000 |
跨域 | 端口不同 |
https://a.com |
http://a.com |
跨域 | 协议不同 |
https://www.test.com |
https://www.baidu.com |
跨域 | 主域不同(test/baidu) |
https://a.test.com |
https://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'})
。回调函数执行,解析响应数据。
示例代码(前端):
js
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) |
有一些请求对服务器有着特殊的要求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。非简单请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求 ( OPTIONS 请求方法) ,称为 "预检"请求 。其作用在于,确认当前网页所在的域名是否在服务器的许可名单中,明确可使用的 HTTP 请求方法和头信息字段。只有在这个请求返回成功的情况下,浏览器才会发出正式的请求。
后端响应示例:
js
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()
方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
js
otherWindow.postMessage(message, targetOrigin,[transfer]);
- message: 将要发送到其他 window 的数据。
- targetOrigin: 通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串
*
(表示无限制)或者一个 URL。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。 - transfer(可选):是一串和 message 同时传递的 Transferable 对象。这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
2.6 document.domain
该方法只适合子域相同,主域不同 的情况,在两个页面都设置 document.domain='子域'
。 比如 a.test.com
和 b.test.com
适用于该方式,只需要给页面添加 document.domain ='test.com'
,表示二级域名都相同就可以实现跨域。 两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。
注意:自Chrome 101版本起,该功能已被废弃,设置操作将导致属性变为可读属性且不再生效。