CORS(Cross-Origin Resource Sharing,跨域资源共享)是一种机制,它使用额外的 HTTP 头来告诉浏览器允许一个源(域、协议、端口)的 Web 应用访问另一个源上的资源。在前后端分离的开发模式中,CORS 几乎是必会遇到的问题。本章将详细解释 CORS 的来龙去脉,以及在 Node.js/Express 中如何正确配置 CORS。
同源策略是浏览器的一项安全功能,它限制了一个源加载的文档或脚本与另一个源的资源进行交互。同源的定义是:协议(protocol)、域名(host)和端口(port)都相同。例如:
http://example.com/page1 和 http://example.com/page2 同源。http://example.com 和 https://example.com 不同源(协议不同)。http://example.com 和 http://api.example.com 不同源(子域名不同)。http://example.com:8080 和 http://example.com:3000 不同源(端口不同)。同源策略限制了一些跨域操作,例如:
XMLHttpRequest 或 fetch 请求。iframe 内容访问)。CORS 提供了一种安全的跨域请求方式,服务器通过特定的响应头来告知浏览器是否允许跨域访问。
CORS 将请求分为两类:简单请求和预检请求。
满足以下所有条件的请求被视为简单请求:
GET、HEAD 或 POST。Accept、Accept-Language、Content-Language、Content-Type(且值为 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)。XMLHttpRequest 对象没有注册任何事件监听器。对于简单请求,浏览器直接发送请求,并在请求头中携带 Origin 字段。服务器根据 Origin 决定是否允许,并在响应头中返回 Access-Control-Allow-Origin 等字段。
对于不满足简单条件的请求(例如使用 PUT、DELETE 方法,或携带自定义头如 Authorization),浏览器会先发送一个 OPTIONS 预检请求,询问服务器是否允许实际请求。预检请求包含以下头:
Access-Control-Request-Method:实际请求的方法。Access-Control-Request-Headers:实际请求携带的自定义头。服务器收到预检请求后,应返回相应的 CORS 头,表明是否允许。如果允许,浏览器才会发送真正的请求。
服务器通过设置以下响应头来控制 CORS 行为:
Access-Control-Allow-Origin:必需。指定允许的源,可以是具体源或 *(通配符,表示所有源,但不能与凭证同时使用)。Access-Control-Allow-Methods:指定允许的 HTTP 方法,用于预检请求响应。Access-Control-Allow-Headers:指定允许的请求头,用于预检请求响应。Access-Control-Allow-Credentials:布尔值,表示是否允许携带凭证(如 Cookie、HTTP 认证)。若为 true,则 Access-Control-Allow-Origin 不能为 *。Access-Control-Expose-Headers:指定哪些响应头可以被客户端访问(默认只有 Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma)。Access-Control-Max-Age:预检请求的缓存时间(秒),在此时间内无需重复预检。Express 应用可以使用 cors 中间件快速实现 CORS 支持。这是最推荐的方案。
安装:
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors()); // 允许所有源
app.get('/api/users', (req, res) => {
res.json([{ name: 'Alice' }]);
});
可以通过传递配置对象来细化 CORS 行为:
const corsOptions = {
origin: 'https://example.com', // 允许的源
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['Content-Range', 'X-Content-Range'],
credentials: true, // 允许携带凭证
maxAge: 600, // 预检请求缓存10分钟
preflightContinue: false,
optionsSuccessStatus: 204
};
app.use(cors(corsOptions));
origin 也可以是一个函数,用于动态决定是否允许:
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = ['https://site1.com', 'https://site2.com'];
if (allowedOrigins.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
};
可以只为某些路由启用 CORS,而不是全局:
app.get('/api/public', cors(), (req, res) => {
res.json({ message: '公开数据' });
});
app.post('/api/secure', cors({ origin: 'https://example.com' }), (req, res) => {
res.json({ message: '受保护数据' });
});
cors 中间件会自动处理 OPTIONS 预检请求,无需额外代码。如果需要手动处理,可以这样:
app.options('/api/users', cors()); // 启用预检
如果想了解底层原理,也可以自己编写 CORS 中间件。以下是一个简单的示例:
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// 处理预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
但处理复杂的动态源、凭证等情况时,使用 cors 库更可靠。
如果跨域请求需要携带 Cookie 或 HTTP 认证,必须:
Access-Control-Allow-Credentials: true。Access-Control-Allow-Origin 不能为 *,必须是明确的源。credentials: 'include'(fetch)或 withCredentials: true(XMLHttpRequest)。示例:
const corsOptions = {
origin: 'https://frontend.com',
credentials: true
};
app.use(cors(corsOptions));
客户端 fetch:
fetch('https://api.example.com/data', {
credentials: 'include'
});
OPTIONS 请求,且 OPTIONS 路由存在。Access-Control-Allow-Origin 不能为 *:明确指定源。Origin 后返回对应值。Access-Control-Allow-Headers 中包含它们。Authorization),需在 Access-Control-Expose-Headers 中声明。开发环境中,常用 Webpack Dev Server 或 Vite 的代理功能避免 CORS 问题。它们通过在开发服务器转发请求到目标 API,使得浏览器认为请求是同源的。但这仅适用于开发环境,生产环境仍需正确配置 CORS 或使用反向代理。
Access-Control-Allow-Origin: *,尤其当 API 需要认证时。Origin,防止凭证被滥用。const express = require('express');
const cors = require('cors');
const app = express();
// 白名单
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com'];
const corsOptions = {
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
app.use(express.json());
// 模拟用户数据
let users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
app.get('/api/users', (req, res) => {
res.json(users);
});
app.post('/api/users', (req, res) => {
const newUser = { id: users.length + 1, name: req.body.name };
users.push(newUser);
res.status(201).json(newUser);
});
app.delete('/api/users/:id', (req, res) => {
const id = parseInt(req.params.id);
users = users.filter(u => u.id !== id);
res.status(204).end();
});
// 处理预检请求(cors 已处理,无需额外)
app.listen(3000, () => console.log('Server running on port 3000'));
前端(fetch)示例:
// 注意:如果需要携带 cookie,需设置 credentials: 'include'
fetch('http://localhost:3000/api/users', {
credentials: 'include'
})
.then(res => res.json())
.then(users => console.log(users));
CORS 是浏览器与服务器协作的安全机制。通过正确配置 CORS 头,可以安全地开放跨域访问。在 Express 中,cors 中间件极大地简化了配置。理解 CORS 的工作原理有助于排查跨域问题和保障应用安全。下一章我们将学习 HTTPS 与更多安全实践。