引言:被边界限制的Web世界
在当今互联网应用中,前端开发者几乎都会遇到一个经典问题:跨域。这不仅是技术层面的挑战,更是浏览器安全机制的核心体现。当我们从https://www.example.com
尝试请求https://api.service.com
的数据时,那道无形的墙便会出现。但这道墙并非为了阻碍而存在,它的背后是整个Web安全体系的基石。
同源策略:浏览器的安全基石
什么是同源策略
同源策略(Same-Origin Policy)是浏览器最核心的安全机制之一,它限制了来自不同"源"的文档或脚本如何相互交互。所谓"同源",指的是协议、域名和端口号完全一致。

为什么需要同源策略
想象一下,如果没有同源策略:
- 恶意网站可以通过iframe嵌入银行网站,并读取用户的操作
- 任意网站都可以通过JavaScript访问其他网站的本地存储数据
- CSRF(跨站请求伪造)攻击将更加难以防范
同源策略就像浏览器为每个网站建立的独立沙箱,保护用户数据不被恶意脚本窃取。
跨域的本质与触发条件
什么是跨域
当请求的URL与当前页面的协议、域名或端口 有任何不同时,就会发生跨域。浏览器
会阻止这类请求的响应数据被JavaScript访问,但请求实际上已经发出。

跨域触发的场景
跨域限制的范围
在请求服务器资源时,不是所有的跨域都会受到限制。主要是针对:
- XMLHttpRequest/Fetch API请求
- Canvas操作跨域图片
- Web Fonts字体加载
- Web Storage和IndexedDB
但是有些情况下是不会受到限制的:
- img标签
- script标签(注意JSONP的安全问题)
- link标签引入CSS
企业级跨域解决方案
CORS
CORS(跨域资源共享)是W3C标准,也是目前最主流的跨域解决方案。对于CORS而言分为简单请求
和预检请求
。
简单请求必须满足如下条件:
- 请求方法为GET、HEAD、POST
- Content-Type为
text/plain
、multipart/form-data
、application/x-www-form-urlcoded
- 没有自定义请求头
js
// fetch默认是GET请求 没有自定义请求头 这是一个简单请求
fetch('https://api.example.com/data')
不满足以上条件的都视为预检请求
。浏览器会先发送OPTIONS预检请求。如果OK则发送真实请求。
js
// 修改Content-Type 为预检请求触发预检
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
CORS方式主要依靠服务器端配置,以Node.js的Express为例
js
const express = require("express");
const app = express();
// CORS中间件
app.use((req, res, next) => {
// 允许的源
const allowedOrigins = [
"https://www.example.com",
"https://admin.example.com",
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin);
}
// 允许的请求方法
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS"
);
// 允许的请求头
res.setHeader(
"Access-Control-Allow-Headers",
"Content-Type, Authorization, X-Custom-Header"
);
// 允许携带凭证
res.setHeader("Access-Control-Allow-Credentials", "true");
// 预检请求缓存时间
res.setHeader("Access-Control-Max-Age", "86400");
if (req.method === "OPTIONS") {
return res.sendStatus(200);
}
next();
});
// 业务路由
app.get("/api/data", (req, res) => {
res.json({ message: "CORS enabled successfully" });
});
app.listen(3000);
由上述代码可知服务器主要在响应头中配置。主要有如下关键配置
- Access-Control-Allow-Origin 允许访问的来源。
*
代表允许任务源,但携带凭证时不能使用*
。还可以指定特定地址 - Access-Control-Allow-Methods 允许请求的HTTP的方法(如GET、POST、PUT)
- Access-Control-Allow-Header 允许的请求头
- Access-Control-Allow-Credentials 布尔值,指示是否允许发送Cookie等凭证信息
- Access-Control-Allow-Max-Age 指定预检请求的有效期(秒为单位),在此期间无需再次发送预检请求
代理服务器方案
在企业开发时,通过代理服务器转发请求是解决跨域的常见方案。作为代理服务器的工具有很多。例如webpack、vite等构建工具都具备这样的能力。
js
// webpack为例 webpack.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
},
secure: false,
onProxyReq: (proxyReq, req, res) => {
// 可以在这里添加认证头等信息
console.log('代理请求:', req.path)
}
}
}
}
}
JSONP
JSONP是利用script标签没有跨域限制的特性实现的。其工作原理如下:
- 前端定义一个回调函数,例如
handleCallback
- 动态的创建一个script标签,其src的地址为请求的URL,并附上回调函数名称为参数,如
?callback=handleCallback
- 服务器接收到请求后,将数据包装在回调函数中返回,如
handleCallback({data: ...})
- 浏览器接收到响应后,会执行这个JS代码,从而触发前端定义的回调函数。
js
function jsonp(url, callbackName) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = `${url}?callback=${callbackName}`;
document.body.appendChild(script);
// 定义全局回调函数
window[callbackName] = function(data) {
resolve(data);
document.body.removeChild(script);
delete window[callbackName];
};
});
}
// 使用
jsonp('http://other-domain.com/data', 'handleCallback').then(data => {
console.log(data);
});
JSONP的方案有如下缺陷:
- 仅支持GET请求
- 安全性差,容易受到XSS攻击
- 需要服务端配合改造
WebSocket
WebSocket 协议本身不受同源策略限制,因为它是一个长连接协议,设计之初就考虑了跨域问题。
工作原理:
浏览器和服务器建立一个 WebSocket 连接后,可以自由地进行双向通信,无需担心跨域。但需要在服务器端支持 WebSocket 协议。
js
// 客户端
const socket = new WebSocket('ws://other-domain.com/socket');
socket.onopen = function(event) {
socket.send('Hello Server!');
};
socket.onmessage = function(event) {
console.log('Message from server:', event.data);
};
其他解决方案
解决跨域还有一些其他的方案,但是现在使用比较少,稍微介绍一下:
- postMessage
主要用于不同窗口(如 iframe、弹出窗口)之间的通信。window.postMessage
方法允许来自不同源的脚本进行安全的异步通信。 - 修改 document.domain (仅限子域)
如果两个页面拥有相同的一级域名 ,但二级域名不同(如a.example.com
和b.example.com
),可以通过将两者的document.domain
都设置为example.com
来实现跨域。适用范围极窄。 - Nginx/Apache 配置 CORS
与在应用服务器代码中设置 CORS 头类似,但可以在 Web 服务器层面统一配置。
小结
跨域问题,本质上是浏览器为了保护用户安全和隐私而建立的同源策略所导致的现象。它并非一个无法逾越的技术障碍,而是一道精心设计的安全边界。通过本文的探讨,我们可以清晰地看到解决这一问题的完整思路:
-
理解根源 :首先要认识到,跨域限制是浏览器的行为,其核心是同源策略。请求通常已经发出并收到了响应,但浏览器拦截了响应数据,不让前端JavaScript代码获取。
-
明确方案:解决方案围绕着"如何安全地绕过同源策略"展开,主要分为两大类:
- 官方标准方案(CORS) :由W3C制定,需要前端与后端协同配合 。通过在HTTP响应头中设置一系列
Access-Control-Allow-*
字段,明确告知浏览器允许哪些源、方法或头部的跨域请求。这是目前最主流、最规范的解决方案。 - 技术变通方案 :利用一些不受同源策略限制的HTML标签(如
<script>
的JSONP )或在开发阶段通过代理服务器转发请求,从而"欺骗"浏览器,实现跨域访问。
- 官方标准方案(CORS) :由W3C制定,需要前端与后端协同配合 。通过在HTTP响应头中设置一系列
-
选择策略:在实际开发中,选择哪种方案取决于具体的场景和技术架构:
- 现代Web应用首选CORS,尤其是需要支持多种HTTP方法和复杂请求头的RESTful API。
- 开发环境 下,使用构建工具(如Webpack、Vite)提供的代理服务器可以避免对后端服务进行修改,提升开发效率。
- 老旧项目或特定场景下,JSONP仍可作为仅支持GET请求的备选方案。
- 需要双向实时通信时,WebSocket是天然支持跨域的优选。
总而言之,解决跨域的过程,是一个在Web安全框架内寻求通信权限的过程。理解其背后的安全原理,远比死记硬背某一种解决方案更为重要。作为一名开发者,我们应在保障安全的前提下,灵活运用这些技术,构建既强大又安全的Web应用。