作为一名 Web 开发者,你是否曾在控制台中见过那令人沮丧的红色报错信息:“Access to XMLHttpRequest at ‘http://localhost:5000‘ from origin ‘http://localhost:3000‘ has been blocked by CORS policy”?
这确实是我们在构建全栈应用时最常遇到的障碍之一。在这篇文章中,我们将不仅仅满足于“解决”报错,而是深入探讨什么是 CORS,以及如何在 Express.js 中优雅、安全地配置它。我们将一起探索从全开放到严格白名单的各种场景,让你在未来的开发中不再惧怕跨域问题。
目录
什么是 CORS?
在深入代码之前,让我们先理解一下我们在对抗什么。你肯定听说过“同源策略”吧?这是浏览器的一项核心安全机制,用来限制一个源的文档或脚本如何能与另一个源的资源进行交互。所谓“同源”,是指两个 URL 具有相同的协议(如 http)、主机名(如 example.com)和端口(如 8080)。
默认情况下,为了安全起见,浏览器会阻止前端代码(比如你在 localhost:3000 运行的 React 应用)去访问不同源的后端 API(比如运行在 localhost:5000 的 Express 服务)。这正是 CORS(跨源资源共享) 登场的时候。
CORS 是一种基于 HTTP 头的机制,它允许服务器通过在响应中包含特定的 HTTP 头,来明确告知浏览器:“嘿,虽然我们不是同一个源,但我允许这个源访问我的资源”。作为开发者,我们的任务就是在 Express 服务器中正确配置这些头部信息。
准备工作:项目搭建
为了更好地演示,让我们一起从零开始构建一个演示项目。我们将创建一个包含服务器端和客户端的完整示例。
第一步:初始化项目
首先,让我们打开终端,创建一个新的目录并初始化 Node.js 应用。为了方便演示,我们将其命名为 cors-demo。
# 创建项目目录并进入
mkdir cors-demo
# 初始化 package.json
npm init -y
第二步:安装依赖
我们将使用 Express 作为服务器框架,并使用官方的 cors 中间件来简化配置。这个中间件是 Express 社区处理跨域问题的标准解决方案。
# 安装 express 和 cors
npm install express cors
第三步:构建目录结构
为了模拟真实的开发场景,我们需要一个服务端和一个客户端。请在你的项目根目录下创建以下结构:
-
server.js:我们的后端服务入口。 -
client/:一个用于存放前端文件的文件夹。
* client/index.html:前端页面。
* client/script.js:前端逻辑。
场景一:允许所有来源(开发环境)
很多时候,尤其是在本地开发阶段,我们希望快速验证功能,而不想被复杂的权限限制住。在这种情况下,我们通常允许所有来源访问我们的 API。请注意,这种方式不建议在生产环境中使用,但在原型设计阶段非常高效。
客户端代码
首先,让我们编写一个简单的前端代码,尝试去请求后端的秘密数据。
client/index.html
CORS 演示页面
body { font-family: sans-serif; padding: 2rem; }
检查控制台获取数据
打开浏览器开发者工具查看请求结果。
client/script.js
// 尝试从我们的后端 API 获取数据
// 注意:由于端口不同,这通常是一个跨域请求
fetch(‘http://localhost:5000/secret‘)
.then(response => response.json())
.then(data => {
console.log(‘成功获取数据:‘, data);
alert(‘秘密数字是: ‘ + data.secret);
})
.catch(error => {
console.error(‘请求失败:‘, error);
});
服务器端代码
现在,让我们来编写服务器端代码。如果不配置 CORS,上面的前端代码会报错。让我们看看如何用最简单的方式解决这个问题。
server.js
// 引入必要的模块
const express = require(‘express‘);
const cors = require(‘cors‘);
// 创建 Express 应用实例
const app = express();
// ============================================
// 核心配置:启用 CORS
// ============================================
// 使用默认配置,允许所有源访问
// 这会添加 Access-Control-Allow-Origin: * 响应头
app.use(cors());
// ============================================
// API 路由定义
// ============================================
// 根路由 - 欢迎信息
app.get(‘/‘, (req, res) => {
res.json({ message: "欢迎来到我们的服务器!" });
});
// 秘密路由 - 模拟敏感数据接口
app.get(‘/secret‘, (req, res) => {
// 生成一个随机秘密数字
const secretNumber = Math.floor(Math.random() * 100);
res.json({ secret: secretNumber });
});
// ============================================
// 启动服务器
// ============================================
const PORT = 5000;
app.listen(PORT, () => {
console.log(`服务器正在端口 ${PORT} 上运行...`);
});
运行测试:
- 启动服务器:
node server.js - 在浏览器中打开 INLINECODE4d74f1f6(你需要使用 Live Server 或类似的简单 HTTP 服务器来运行 HTML 文件,直接双击打开文件可能因为 INLINECODE8dd6f42a 协议导致其他问题,但为了演示 CORS,确保使用 http 协议访问前端)。
如果你看到控制台输出了秘密数字,恭喜你!CORS 已经成功配置。
场景二:特定源白名单(生产环境推荐)
在实际的生产环境中,允许所有源(INLINECODE23c828be)通常不是一个好主意,因为这可能会导致安全风险或 CSRF 攻击。通常,我们会明确知道前端应用的域名,例如 INLINECODE1109980f 或 http://localhost:3000。
我们可以通过配置 origin 选项来实现这一点。
高级配置代码示例
让我们修改 server.js,只允许特定的源访问我们的资源。
server.js (更新版)
const express = require(‘express‘);
const cors = require(‘cors‘);
const app = express();
// ============================================
// CORS 配置选项
// ============================================
const corsOptions = {
// 这是一个函数,用来动态检查请求的 origin 是否在白名单中
origin: function (origin, callback) {
// 允许的源列表(白名单)
const whitelist = [
‘http://localhost:5500‘, // 你的本地开发前端地址
‘http://127.0.0.1:5500‘,
‘https://my-production-app.com‘ // 你的线上前端地址
];
// 注意:在某些请求中(如移动端 App 或 Postman),origin 可能为 undefined
// 这里我们根据实际需求决定是否允许无 origin 的请求
if (whitelist.indexOf(origin) !== -1 || !origin) {
callback(null, true); // 允许访问
} else {
callback(new Error(‘Not allowed by CORS‘)); // 拒绝访问
}
},
// 是否允许发送 Cookie (凭证)
// 如果设为 true,origin 不能为 ‘*‘
credentials: true,
// 预检请求的有效期(秒)
optionsSuccessStatus: 200
};
// 将配置传入 cors 中间件
app.use(cors(corsOptions));
// 测试路由
app.get(‘/‘, (req, res) => {
res.json({ message: "欢迎访问安全的服务器!" });
});
app.get(‘/secret‘, (req, res) => {
const data = { secret: "只有白名单内的网站能看到这个", user: "Admin" };
res.json(data);
});
// 错误处理中间件(用于处理 CORS 拒绝导致的错误)
app.use(function (err, req, res, next) {
if (err) {
// 这里的错误通常是 CORS 阻止了请求,所以浏览器可能收不到这个响应
// 但对于非浏览器的请求(如 Postman),这很有用
res.status(500).send(‘CORS 错误: ‘ + err.message);
}
});
const PORT = 5000;
app.listen(PORT, () => {
console.log(`安全服务器正在运行在 ${PORT}`);
});
在这个配置中,我们使用了 INLINECODE02e0074a 函数。这是一种非常强大的模式。它允许我们在运行时动态决定是否允许某个请求。如果请求头中的 INLINECODE6fd3e758 在我们的白名单中,我们就调用 callback(null, true) 允许它;否则,我们抛出一个错误。
场景三:针对单个路由启用 CORS
有时,我们可能希望大多数 API 保持私有(只允许同源访问),但只对某一个特定的公共接口开放跨域访问。cors 中间件非常灵活,我们可以把它作为中间件参数传递给特定的路由,而不是全局挂载。
示例:
const express = require(‘express‘);
const cors = require(‘cors‘);
const app = express();
// 全局不启用 CORS
// 这意味着默认情况下,所有路由都是受同源策略保护的
// 公共接口:允许任何人访问
// 这个接口使用了 cors() 中间件
app.get(‘/public-data‘, cors(), (req, res) => {
res.json({
msg: "这是一个公共 API,任何人都可以跨域访问我!",
data: [1, 2, 3]
});
});
// 私有接口:不允许跨域
// 这个接口没有使用 cors(),因此如果前端尝试跨域请求,浏览器会阻止它
app.get(‘/private-data‘, (req, res) => {
res.json({
msg: "这是一个私有 API,只有同源的前端才能访问。"
});
});
const PORT = 5000;
app.listen(PORT, () => {
console.log(`服务器运行在端口 ${PORT}`);
});
这种“按需启用”的策略在微服务架构或混合应用中非常有用,可以最大程度地保证安全性。
场景四:处理带凭证的请求(Cookies)
这是一个非常容易踩坑的地方。如果你需要在跨域请求中发送 Cookies(例如使用 INLINECODEac855713 时设置 INLINECODE02e47b66),你需要进行额外的配置。
关键点:
- 服务器的 CORS 配置中必须设置
credentials: true。 - 服务器的 INLINECODE05f7cf74 不能设置为通配符 INLINECODE629f5bb4,必须指定具体的域名(如上面提到的白名单数组)。
前端代码示例:
// 前端 fetch 必须带 credentials
fetch(‘http://localhost:5000/user-profile‘, {
method: ‘GET‘,
credentials: ‘include‘, // 关键:告诉浏览器要在请求中包含 Cookie
headers: {
‘Content-Type‘: ‘application/json‘
}
})
.then(res => res.json())
.then(data => console.log(‘用户数据:‘, data));
后端配置示例:
const corsOptions = {
origin: ‘http://localhost:5500‘, // 必须指定具体源,不能用 ‘*‘
credentials: true, // 关键:允许凭证
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
app.get(‘/user-profile‘, (req, res) => {
// 这里的逻辑可以访问 req.cookies
res.json({ username: "开发者123", loggedIn: true });
});
常见误区与故障排查
在开发过程中,即使配置了 CORS,你仍然可能遇到问题。以下是一些我们总结的经验和常见错误:
1. 预检请求(Preflight)失败
当你发送的请求不仅仅是简单的 GET 或 POST,或者你添加了自定义的 HTTP 头(如 INLINECODEd4053845),浏览器会先发送一个 INLINECODE9cf103bb 请求(称为“预检请求”),询问服务器是否允许该实际请求。
如果你的控制台报错 Request header field x-custom-header is not allowed by Access-Control-Allow-Headers,这意味着你需要配置允许的头部。
解决方案:
const corsOptions = {
origin: ‘*‘,
// 允许的请求头
allowedHeaders: [‘Content-Type‘, ‘Authorization‘, ‘x-custom-token‘],
// 暴露给客户端的响应头(允许客户端读取的响应头)
exposedHeaders: ‘x-total-count‘,
// 允许的 HTTP 方法
methods: ‘GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS‘
};
app.use(cors(corsOptions));
2. 代理方式绕过 CORS
在前端开发中(比如使用 Create React App 或 Vue CLI),我们可以配置开发服务器的代理。这实际上是将浏览器的请求发给了同源的开发服务器,再由开发服务器转发给后端。
package.json (前端项目) 中的配置示例:
"proxy": "http://localhost:5000"
这样,前端在请求 INLINECODE9fbfd1c5 时,实际上会被代理到 INLINECODE39d559c1。因为对浏览器来说,请求是发给同源的开发服务器的,所以不存在跨域问题。但这只适用于开发环境。
总结
在这篇文章中,我们一起深入探讨了 CORS 的方方面面。从理解“同源策略”开始,我们搭建了项目,实践了四种不同的配置场景:全局开放、白名单限制、单路由配置以及凭证处理。
CORS 并不神秘,它只是浏览器和服务器之间的一种约定。掌握了 Express 中的 cors 中间件配置,你就掌握了全栈开发连接前后端的关键钥匙。建议你在生产环境中始终使用白名单配置,并谨慎处理凭证,以确保应用的安全。
希望这篇文章能帮助你解决遇到的跨域难题。编码愉快!