Node系列 · Express:session
Session 是传统服务端会话方案——登录后服务端存一份"会话状态",客户端只持有 sessionId。本章对比 Cookie 与 Session、Express 用 express-session 实现登录态管理。
一、Cookie vs Session
| 维度 | Cookie | Session |
|---|---|---|
| 数据存储 | 客户端 | 服务端 |
| 容量 | 4KB | 无限制(看存储后端) |
| 安全性 | 数据暴露在客户端 | 数据在服务端,客户端只有 ID |
| 实现复杂度 | 简单 | 需要 session 存储 |
| 服务端扩展 | 无状态(多实例无障碍) | 多实例需要共享 session 存储 |
| 适用 | 不敏感数据、会话标识 | 登录态、用户偏好 |
TIP
登录态首选 Session——密码、用户信息等敏感数据不能放 Cookie。Session 的 ID 只是个随机字符串,泄露后服务端可主动失效。
二、express-session 基础
bash
npm install express-sessionjavascript
const session = require('express-session');
app.use(session({
secret: 'keyboard cat', // 签名密钥(必填,生产用随机字符串)
resave: false, // 强制重新保存(即使未修改)
saveUninitialized: false, // 强制保存空 session
cookie: {
httpOnly: true, // 防 XSS
secure: true, // 仅 HTTPS(生产)
sameSite: 'lax', // 防 CSRF
maxAge: 24 * 60 * 60 * 1000, // 24 小时
},
}));
// 使用
app.post('/api/login', (req, res) => {
req.session.userId = user.id; // 存 session
res.json({ user });
});
app.get('/api/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: '未登录' });
}
// ...
});三、关键配置项
3.1 secret
用于签名 sessionId,防篡改:
javascript
// 生产环境:随机长字符串
app.use(session({
secret: process.env.SESSION_SECRET || require('crypto').randomBytes(32).toString('hex'),
}));3.2 resave 与 saveUninitialized
| 选项 | 推荐 | 说明 |
|---|---|---|
resave | false | 请求未修改 session 时不重写存储——性能更好 |
saveUninitialized | false | 不保存空 session——避免无意义的存储写入 |
3.3 cookie 选项
完整配置:
javascript
cookie: {
httpOnly: true, // 防 XSS
secure: process.env.NODE_ENV === 'production', // 仅 HTTPS(生产)
sameSite: 'lax', // 防 CSRF
maxAge: 24 * 60 * 60 * 1000, // 24 毫秒单位
domain: '.example.com', // 子域共享(可选)
path: '/', // 路径
}3.4 滚动会话
rolling: true 让 session 在每次请求时自动续期——活跃用户不会突然过期:
javascript
app.use(session({
rolling: true,
cookie: { maxAge: 60 * 60 * 1000 }, // 每次请求重置 1 小时
}));WARNING
滚动会话有安全权衡:活跃用户一直不过期,但 token 长期有效——被盗后攻击者能长期使用。需要结合 IP / User-Agent 检测。
四、session 存储后端
默认 express-session 把 session 存在内存——多实例部署会失效(每个进程一份内存)。
4.1 内存存储(默认)
javascript
// 不指定 store 就是内存存储
app.use(session({ secret: 'xxx' }));适用:单实例开发。生产禁用——重启丢所有 session。
4.2 Redis 存储(生产首选)
bash
npm install redis connect-redisjavascript
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({ url: 'redis://127.0.0.1:6379' });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'xxx',
resave: false,
saveUninitialized: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 },
}));Redis 优势:
- 多实例共享 session(任意 Node 进程都能读到同一份 session)
- 自动过期(Redis TTL)
- 重启不丢数据
4.3 数据库存储
bash
npm install connect-session-sequelizejavascript
const session = require('express-session');
const SequelizeStore = require('connect-session-sequelize')(session.Store);
const { sequelize } = require('./models');
app.use(session({
store: new SequelizeStore({ db: sequelize }),
secret: 'xxx',
resave: false,
saveUninitialized: false,
}));会自动创建 Sessions 表——MySQL/PostgreSQL 都能用。
五、session 操作 API
javascript
// 存
req.session.userId = 1;
req.session.cart = [1, 2, 3];
// 读
const userId = req.session.userId;
// 重新生成 sessionId(防会话固定)
req.session.regenerate((err) => {
req.session.userId = userId;
});
// 销毁 session(退出登录)
req.session.destroy((err) => {
res.clearCookie('connect.sid');
res.json({ ok: true });
});
// 重新加载 session(多步操作时)
req.session.reload((err) => {
console.log(req.session);
});5.1 登录后 regenerate
javascript
app.post('/login', async (req, res) => {
// 1. 验证用户
const user = await authenticate(req.body);
// 2. 登录成功,重新生成 sessionId(防会话固定)
req.session.regenerate((err) => {
if (err) return next(err);
req.session.userId = user.id;
req.session.save((err) => {
res.json({ user });
});
});
});TIP
登录后 regenerate sessionId 是安全最佳实践——防止"会话固定攻击"(攻击者预先设一个 sessionId 让受害者用,登录后劫持会话)。
六、退出登录
javascript
app.post('/api/logout', (req, res) => {
req.session.destroy((err) => {
if (err) return next(err);
res.clearCookie('connect.sid'); // 清除 session cookie
res.json({ ok: true });
});
});七、最佳实践
| 场景 | 推荐 |
|---|---|
| 开发环境 | 默认内存存储即可 |
| 生产环境 | Redis 存储(多实例共享 + 自动过期) |
| 登录后 | req.session.regenerate() 防会话固定 |
| 退出 | req.session.destroy() + res.clearCookie() |
| Cookie 配置 | httpOnly + secure(生产)+ sameSite: 'lax' |
| secret | 随机长字符串,存环境变量 |
| 多实例 | 必须用 Redis 等共享存储 |
八、与 JWT 的取舍
| 维度 | Session | JWT |
|---|---|---|
| 数据存储 | 服务端 | 客户端(payload) |
| 多实例 | 需要共享存储 | 无状态,天然支持 |
| 主动失效 | ✅(服务端删 session) | ❌(要等过期或拉黑名单) |
| 实现 | 较重 | 轻 |
| 适用 | 传统 Web 应用 | 前后端分离 / 移动端 / 微服务 |
TIP
传统 Web 用 Session——后台能主动踢人(封号、改密)。API / 移动端 / 微服务用 JWT——天然无状态、跨域友好。详见 JWT 章节。
九、小结
- Session 把数据存服务端,客户端只持 sessionId——比 Cookie 安全
express-session是标准中间件;生产用 Redis 存储支持多实例- 关键配置:
secret(签名密钥)+resave: false+saveUninitialized: false+ Cookie 安全属性 - 登录后
regeneratesessionId 防会话固定;退出destroy()+clearCookie() - 多实例部署必须用共享存储(Redis / 数据库),不能用默认内存
- Session 与 JWT:传统 Web 用 Session,API/移动端用 JWT
