Passport.js 会话管理 #
什么是会话? #
会话(Session)是一种在服务器端存储用户状态的机制。由于 HTTP 协议是无状态的,每次请求都是独立的,会话允许我们在多个请求之间保持用户状态。
text
┌─────────────────────────────────────────────────────────────┐
│ 会话工作原理 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 第一次请求 │
│ ┌─────────┐ ┌─────────┐ │
│ │ 客户端 │ ──── 请求 ────────>│ 服务器 │ │
│ └─────────┘ └────┬────┘ │
│ ▲ │ │
│ │ │ 创建会话 │
│ │ ▼ │
│ │ ┌─────────┐ │
│ └───── Set-Cookie ────────│ Session │ │
│ session_id=abc123 │ Store │ │
│ │ │
│ 后续请求 │ │
│ ┌─────────┐ │ │
│ │ 客户端 │ ──── Cookie ─────────>│ 服务器 │ │
│ │session_id=abc123│ └────┬────┘ │
│ └─────────┘ │ │
│ ▲ │ 查找会话 │
│ │ ▼ │
│ │ ┌─────────┐ │
│ └───────────────────────────│ Session │ │
│ │ Store │ │
│ └─────────┘ │
└─────────────────────────────────────────────────────────────┘
Passport.js 会话机制 #
核心概念 #
Passport.js 使用会话来保持用户的登录状态。主要涉及两个关键函数:
- 序列化(serializeUser):将用户对象转换为可存储的形式
- 反序列化(deserializeUser):从存储中恢复用户对象
text
┌─────────────────────────────────────────────────────────────┐
│ Passport 会话流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 登录成功 │
│ │ │
│ ▼ │
│ serializeUser(user) │
│ │ │
│ │ 提取 user.id │
│ ▼ │
│ 存储到 Session: req.session.passport.user = user.id │
│ │ │
│ ▼ │
│ 后续请求 │
│ │ │
│ ▼ │
│ 读取 Session: req.session.passport.user │
│ │ │
│ ▼ │
│ deserializeUser(id) │
│ │ │
│ │ 根据 id 查询用户 │
│ ▼ │
│ 恢复用户对象: req.user = user │
│ │
└─────────────────────────────────────────────────────────────┘
基本配置 #
javascript
// app.js
const express = require('express');
const session = require('express-session');
const passport = require('passport');
const app = express();
// 会话配置
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24 小时
}
}));
// Passport 初始化
app.use(passport.initialize());
app.use(passport.session());
// 序列化用户
passport.serializeUser((user, done) => {
done(null, user.id);
});
// 反序列化用户
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (error) {
done(error, null);
}
});
序列化与反序列化 #
基本实现 #
javascript
// 最简单的方式:存储用户 ID
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (error) {
done(error, null);
}
});
存储更多信息 #
javascript
// 存储更多用户信息,减少数据库查询
passport.serializeUser((user, done) => {
done(null, {
id: user.id,
role: user.role,
username: user.username
});
});
passport.deserializeUser(async (data, done) => {
try {
// 可以直接使用存储的信息
// 或者只查询必要的信息
const user = await User.findById(data.id).select('username email role');
done(null, user);
} catch (error) {
done(error, null);
}
});
性能优化 #
javascript
// 使用缓存减少数据库查询
const NodeCache = require('node-cache');
const userCache = new NodeCache({ stdTTL: 600 }); // 10 分钟缓存
passport.deserializeUser(async (id, done) => {
try {
// 先从缓存获取
let user = userCache.get(id);
if (user) {
return done(null, user);
}
// 缓存未命中,查询数据库
user = await User.findById(id);
if (user) {
userCache.set(id, user);
}
done(null, user);
} catch (error) {
done(error, null);
}
});
// 用户信息更新时清除缓存
function clearUserCache(userId) {
userCache.del(userId);
}
Session Store 配置 #
内存存储(开发环境) #
javascript
// 默认使用内存存储,仅适合开发环境
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 }
}));
Redis 存储(生产环境推荐) #
bash
npm install connect-redis redis
javascript
// app.js
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
// 创建 Redis 客户端
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
redisClient.connect().catch(console.error);
// 会话配置
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000
}
}));
MongoDB 存储 #
bash
npm install connect-mongo
javascript
const MongoStore = require('connect-mongo');
app.use(session({
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI,
collectionName: 'sessions',
ttl: 24 * 60 * 60, // 24 小时
autoRemove: 'native'
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false
}));
PostgreSQL 存储 #
bash
npm install connect-pg-simple
javascript
const pgSession = require('connect-pg-simple')(session);
app.use(session({
store: new pgSession({
conString: process.env.DATABASE_URL,
tableName: 'user_sessions'
}),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { maxAge: 24 * 60 * 60 * 1000 }
}));
会话安全 #
Cookie 安全配置 #
javascript
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
// 仅 HTTPS
secure: process.env.NODE_ENV === 'production',
// 防止 JavaScript 访问
httpOnly: true,
// 同站策略
sameSite: 'strict', // 或 'lax'
// 有效期
maxAge: 24 * 60 * 60 * 1000,
// 域名
domain: process.env.COOKIE_DOMAIN,
// 路径
path: '/'
}
}));
会话固定攻击防护 #
javascript
// 登录成功后重新生成会话
router.post('/login', (req, res, next) => {
passport.authenticate('local', (err, user, info) => {
if (err) return next(err);
if (!user) {
return res.status(401).json(info);
}
// 重新生成会话 ID
req.session.regenerate((err) => {
if (err) return next(err);
req.logIn(user, (err) => {
if (err) return next(err);
res.json({ success: true, user });
});
});
})(req, res, next);
});
会话超时处理 #
javascript
// 活动会话更新
app.use((req, res, next) => {
if (req.session && req.user) {
// 检查会话是否即将过期
const now = Date.now();
const expiresAt = req.session.cookie.expires?.getTime() || 0;
const remaining = expiresAt - now;
// 如果剩余时间少于 30 分钟,刷新会话
if (remaining < 30 * 60 * 1000 && remaining > 0) {
req.session.touch();
}
}
next();
});
// 绝对超时
app.use((req, res, next) => {
if (req.session && req.user) {
const loginTime = req.session.loginTime;
const maxAbsoluteAge = 8 * 60 * 60 * 1000; // 8 小时绝对超时
if (loginTime && Date.now() - loginTime > maxAbsoluteAge) {
req.logout();
req.session.destroy();
return res.redirect('/auth/login?timeout=true');
}
}
next();
});
// 登录时记录时间
passport.serializeUser((user, done) => {
done(null, { id: user.id, loginTime: Date.now() });
});
会话管理功能 #
查看活跃会话 #
javascript
// 获取用户所有活跃会话
router.get('/sessions', ensureAuthenticated, async (req, res) => {
const sessions = await Session.find({
'session.passport.user.id': req.user.id
});
res.render('sessions', { sessions });
});
// 撤销特定会话
router.delete('/sessions/:sessionId', ensureAuthenticated, async (req, res) => {
const { sessionId } = req.params;
// 不能撤销当前会话
if (sessionId === req.sessionID) {
return res.status(400).json({ error: 'Cannot revoke current session' });
}
await Session.findByIdAndDelete(sessionId);
res.json({ success: true });
});
// 撤销所有其他会话
router.delete('/sessions', ensureAuthenticated, async (req, res) => {
await Session.deleteMany({
'session.passport.user.id': req.user.id,
_id: { $ne: req.sessionID }
});
res.json({ success: true });
});
多设备登录控制 #
javascript
// 限制同时登录设备数
const MAX_SESSIONS = 3;
router.post('/login', async (req, res, next) => {
passport.authenticate('local', async (err, user, info) => {
if (err) return next(err);
if (!user) return res.status(401).json(info);
// 检查现有会话数
const existingSessions = await Session.countDocuments({
'session.passport.user.id': user.id
});
if (existingSessions >= MAX_SESSIONS) {
// 删除最旧的会话
const oldestSession = await Session.findOne({
'session.passport.user.id': user.id
}).sort({ _id: 1 });
if (oldestSession) {
await Session.findByIdAndDelete(oldestSession._id);
}
}
req.logIn(user, (err) => {
if (err) return next(err);
res.json({ success: true, user });
});
})(req, res, next);
});
无会话认证 #
API 认证(无会话) #
对于 API 服务,可以使用无会话认证:
javascript
// app.js
app.use(passport.initialize());
// 不使用 passport.session()
// 使用 JWT 策略
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.JWT_SECRET
}, async (payload, done) => {
try {
const user = await User.findById(payload.sub);
if (user) {
return done(null, user);
}
return done(null, false);
} catch (error) {
return done(error, false);
}
}));
// API 路由
router.get('/profile', passport.authenticate('jwt', { session: false }), (req, res) => {
res.json(req.user);
});
会话调试 #
调试中间件 #
javascript
// 开发环境调试
if (process.env.NODE_ENV === 'development') {
app.use((req, res, next) => {
console.log('Session ID:', req.sessionID);
console.log('Session:', req.session);
console.log('User:', req.user);
next();
});
}
会话状态检查 #
javascript
// 检查会话状态的路由
router.get('/session-status', (req, res) => {
res.json({
isAuthenticated: req.isAuthenticated(),
sessionId: req.sessionID,
user: req.user || null,
session: {
cookie: req.session.cookie,
passport: req.session.passport
}
});
});
生产环境配置 #
完整配置示例 #
javascript
// config/session.js
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const redisClient = createClient({
url: process.env.REDIS_URL,
socket: {
reconnectStrategy: (retries) => {
if (retries > 10) {
console.error('Redis connection failed');
return new Error('Redis connection failed');
}
return Math.min(retries * 100, 3000);
}
}
});
redisClient.connect().catch(console.error);
redisClient.on('error', (err) => {
console.error('Redis Client Error:', err);
});
redisClient.on('connect', () => {
console.log('Redis connected');
});
const sessionConfig = {
store: new RedisStore({
client: redisClient,
prefix: 'sess:'
}),
secret: process.env.SESSION_SECRET,
name: 'sessionId', // 自定义 Cookie 名称
resave: false,
saveUninitialized: false,
rolling: true, // 每次请求刷新 Cookie
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000,
domain: process.env.COOKIE_DOMAIN
}
};
module.exports = session(sessionConfig);
javascript
// app.js
const express = require('express');
const passport = require('passport');
const sessionMiddleware = require('./config/session');
const app = express();
// 信任代理(如果使用反向代理)
app.set('trust proxy', 1);
// 会话中间件
app.use(sessionMiddleware);
// Passport
app.use(passport.initialize());
app.use(passport.session());
常见问题 #
1. 会话丢失 #
javascript
// 确保 session 中间件在 passport 之前
app.use(session({...}));
app.use(passport.initialize());
app.use(passport.session());
2. 序列化错误 #
javascript
// 确保用户对象存在
passport.serializeUser((user, done) => {
if (!user || !user.id) {
return done(new Error('Invalid user object'));
}
done(null, user.id);
});
3. 跨域会话 #
javascript
// CORS 配置
const cors = require('cors');
app.use(cors({
origin: process.env.CLIENT_URL,
credentials: true // 允许携带 Cookie
}));
下一步 #
现在你已经掌握了会话管理,接下来学习 JWT认证,实现无状态 API 认证!
最后更新:2026-03-28