在 Web 应用发展的早期,Session 和 Cookie 就是实现用户状态管理的基础设施。即使在 JWT 流行的今天,它们仍然广泛应用于传统服务端渲染的应用中。本章将深入讲解 Cookie 和 Session 的工作原理,以及在 Express 中如何使用它们构建安全的用户认证系统。
HTTP 协议是无状态的,这意味着服务器无法识别连续请求是否来自同一个用户。为了维护用户状态(如登录状态、购物车信息),我们需要一种机制在客户端和服务器之间传递标识信息。这就是 Cookie 和 Session 的用武之地。
Cookie 是由服务器发送给客户端(浏览器)的一小段数据,浏览器会存储它,并在后续请求中自动携带(通常通过 Cookie 请求头)。Cookie 主要用于会话管理、个性化设置和跟踪用户行为。
Strict、Lax 或 None。Express 原生支持设置 Cookie,但读取请求中的 Cookie 需要 cookie-parser 中间件。
npm install cookie-parser
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
app.use(cookieParser());
app.get('/set-cookie', (req, res) => {
// 设置一个 Cookie,过期时间为 1 天
res.cookie('username', 'john', { maxAge: 24 * 60 * 60 * 1000, httpOnly: true });
res.send('Cookie 已设置');
});
app.get('/get-cookie', (req, res) => {
const username = req.cookies.username;
res.send(`Cookie 中的用户名是: ${username}`);
});
Session 是服务器端存储的用户特定数据。服务器为每个用户创建一个唯一的会话 ID(通常存储在 Cookie 中),并通过该 ID 关联会话数据。当用户发送请求时,浏览器自动携带包含会话 ID 的 Cookie,服务器根据 ID 查找对应的会话数据。
Session 数据可以存储在内存、文件、数据库(如 Redis、MongoDB)中,生产环境通常使用 Redis 等外部存储以实现多实例共享。
express-session 是 Express 生态中最流行的会话中间件。它负责生成会话 ID、存储会话数据、设置 Cookie 等。
安装:
npm install express-session
const session = require('express-session');
app.use(session({
secret: 'your-secret-key', // 用于签名会话 ID Cookie 的密钥
resave: false, // 是否每次请求都重新保存会话
saveUninitialized: false, // 是否保存未初始化的会话
cookie: {
secure: false, // 如果为 true,则仅通过 HTTPS 发送 Cookie
httpOnly: true, // 防止 XSS 攻击
maxAge: 1000 * 60 * 60 * 24 // 24 小时
}
}));
关键选项说明:
secret:用于签名 Cookie 的密钥,防止篡改。生产环境应使用强密钥,并存储在环境变量中。resave:通常设为 false,避免每次请求都重新保存未修改的会话。saveUninitialized:设为 false 可减少存储未登录用户的空会话。cookie:设置 Cookie 的属性,与 res.cookie 类似。会话数据被挂载到 req.session 对象上,可以像普通对象一样读写。
// 登录处理
app.post('/login', (req, res) => {
const { username, password } = req.body;
// 假设验证通过
req.session.userId = 123;
req.session.username = username;
res.send('登录成功');
});
// 受保护路由
app.get('/dashboard', (req, res) => {
if (req.session.userId) {
res.send(`欢迎回来,${req.session.username}`);
} else {
res.status(401).send('请先登录');
}
});
// 注销
app.post('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.status(500).send('注销失败');
}
res.clearCookie('connect.sid'); // 清除会话 Cookie(默认名为 connect.sid)
res.send('已注销');
});
});
默认情况下,express-session 将会话数据存储在内存中。这会导致:
因此生产环境应使用外部存储,如 Redis、MongoDB 等。express-session 支持通过 store 选项接入多种存储引擎。
例如使用 connect-redis:
npm install redis connect-redis
const redis = require('redis');
const RedisStore = require('connect-redis')(session);
const redisClient = redis.createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your-secret',
resave: false,
saveUninitialized: false
}));
| 特性 | Session | JWT |
|---|---|---|
| 存储位置 | 服务器端(内存/数据库) | 客户端(浏览器) |
| 状态性 | 有状态(服务器需要维护会话) | 无状态(服务器只需验证签名) |
| 扩展性 | 需要共享存储(如 Redis)才能跨服务器 | 天然支持分布式,只需所有服务共享密钥 |
| 安全性 | Cookie 可设为 HttpOnly,防御 XSS 较好;但需防范 CSRF | 若存储在 localStorage,易受 XSS 攻击;使用 HttpOnly Cookie 可缓解 |
| 注销机制 | 直接销毁服务器会话即可 | 需维护黑名单或依赖短时效 + 刷新 token |
secure: true,确保 Cookie 仅通过加密连接传输。Lax 或 Strict 可有效缓解 CSRF 攻击。req.session.regenerate())。下面是一个完整的 Express 应用,演示基于 Session 的认证:
const express = require('express');
const session = require('express-session');
const cookieParser = require('cookie-parser');
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
app.use(session({
secret: 'my-secret-key',
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, maxAge: 1000 * 60 * 30 } // 30分钟
}));
// 模拟用户数据库
const users = [
{ id: 1, username: 'alice', password: '123456' },
{ id: 2, username: 'bob', password: 'abcdef' }
];
// 登录页面(简易 HTML)
app.get('/login', (req, res) => {
res.send(`
<form method="POST" action="/login">
<input type="text" name="username" placeholder="用户名">
<input type="password" name="password" placeholder="密码">
<button type="submit">登录</button>
</form>
`);
});
// 处理登录
app.post('/login', (req, res) => {
const { username, password } = req.body;
const user = users.find(u => u.username === username && u.password === password);
if (user) {
req.session.userId = user.id;
req.session.username = user.username;
res.redirect('/profile');
} else {
res.send('用户名或密码错误');
}
});
// 受保护页面
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.redirect('/login');
}
res.send(`
<h1>欢迎 ${req.session.username}</h1>
<a href="/logout">注销</a>
`);
});
// 注销
app.get('/logout', (req, res) => {
req.session.destroy(err => {
if (err) {
return res.send('注销失败');
}
res.clearCookie('connect.sid');
res.redirect('/login');
});
});
app.listen(3000, () => console.log('Server running on port 3000'));
csurf 中间件)。