如何借助Passport实现跨服务器重启存储已连接用户会话?
实现Passport-Local结合MongoDB会话存储的方案
你提到的需求其实需要把Passport的身份验证和Express会话的持久化存储结合起来——Passport本身只负责验证用户身份,会话的创建、存储和管理是由express-session和对应的存储中间件(比如connect-mongo)来处理的。下面我一步步帮你实现需求,同时修正你代码里的小问题:
第一步:修正并完善MongoDB Schema
首先你的Schema有几个小问题,我先调整一下:
import passport from 'passport' import LocalStrategy from 'passport-local' import mongoose from 'mongoose' import session from 'express-session' import MongoStore from 'connect-mongo' // 需要安装这个包:npm install connect-mongo // 用户Schema const UserSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, password: { type: String, required: true } }) // 会话Schema(注意命名和字段修正) const SessionSchema = new mongoose.Schema({ // 会话ID,express-session会自动填充这个字段为session的_id session: { type: String, required: true }, // 关联到User集合的ObjectId userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }, ip: { type: String, required: true }, lastSeen: { type: Date, default: Date.now } }) // 创建Model const User = mongoose.model('User', UserSchema) const Session = mongoose.model('Session', SessionSchema)
第二步:配置Express会话和MongoDB存储
接下来要把express-session和connect-mongo配置好,让会话数据存在你的Session集合里:
// 先连接MongoDB await mongoose.connect('mongodb://localhost:27017/your-db-name') // 配置会话中间件 app.use(session({ secret: 'your-strong-secret-key-here', // 用于签名会话ID,一定要保密 resave: false, // 不要强制保存未修改的会话 saveUninitialized: false, // 不要保存未初始化的会话(比如未登录的用户) cookie: { maxAge: 24 * 60 * 60 * 1000, // 会话有效期1天(自动登出未活动用户的基础) rolling: true // 每次用户活动时刷新cookie有效期 }, // 用connect-mongo把会话存在MongoDB store: MongoStore.create({ mongoUrl: 'mongodb://localhost:27017/your-db-name', collectionName: 'sessions', // 对应我们的Session集合 stringify: false // 不要自动序列化,保持原始对象 }) }))
第三步:配置Passport并关联会话
Passport需要序列化/反序列化用户,同时在用户登录成功后,我们要把额外的信息(IP、用户ID)存入会话:
// 初始化Passport app.use(passport.initialize()) app.use(passport.session()) // 序列化用户:把用户ID存在会话里 passport.serializeUser((user, done) => { done(null, user._id) }) // 反序列化用户:从会话里的用户ID找到对应的用户 passport.deserializeUser(async (id, done) => { try { const user = await User.findById(id) done(null, user) } catch (err) { done(err) } }) // 配置LocalStrategy passport.use(new LocalStrategy(async (username, password, done) => { try { const user = await User.findOne({ username }) if (!user) return done(null, false, { message: '用户名不存在' }) // 这里要替换成你的密码验证逻辑(比如bcrypt.compare) if (user.password !== password) return done(null, false, { message: '密码错误' }) // 登录成功后,记录会话的IP和用户ID const sessionId = req.sessionID // express-session自动生成的会话ID await Session.create({ session: sessionId, userId: user._id, ip: req.ip, lastSeen: new Date() }) return done(null, user) } catch (err) { return done(err) } }))
第四步:实现你的核心需求
现在我们来逐个实现你提到的目标:
1. 了解当前已连接用户
直接查询Session集合,关联User即可:
// 获取所有在线用户 const onlineUsers = await Session.find().populate('userId', 'username')
2. 检测IP变更(安全)
添加一个全局中间件,每次请求时检查会话的IP是否和当前请求IP一致:
app.use(async (req, res, next) => { if (req.isAuthenticated()) { const session = await Session.findOne({ session: req.sessionID }) if (session && session.ip !== req.ip) { // IP变更,销毁会话并登出用户 req.logout(() => { req.session.destroy(() => { res.redirect('/login?error=ip-change') }) }) return } } next() })
3. 自动登出长时间未活动的用户
我们已经在会话配置里设置了cookie.maxAge和rolling: true,用户活动时会自动刷新有效期。另外可以加一个定时任务清理过期会话:
// 每天清理一次过期会话 setInterval(async () => { const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000) await Session.deleteMany({ lastSeen: { $lt: cutoff } }) }, 24 * 60 * 60 * 1000)
4. 每次请求更换会话编号(安全)
在用户登录成功后,或者每次请求时调用req.session.regenerate()更换会话ID:
// 登录路由示例 app.post('/login', passport.authenticate('local', { failureRedirect: '/login' }), async (req, res) => { // 登录成功后更换会话ID req.session.regenerate(async (err) => { if (err) return res.redirect('/login') // 更新会话记录里的session ID await Session.findOneAndUpdate( { userId: req.user._id }, { session: req.sessionID, lastSeen: new Date() } ) res.redirect('/dashboard') }) }) // 或者全局中间件,每次请求更换(注意:太频繁可能影响性能,建议只在敏感操作后更换) app.use(async (req, res, next) => { if (req.isAuthenticated()) { const oldSessionId = req.sessionID req.session.regenerate(async (err) => { if (err) return next(err) // 更新会话记录 await Session.findOneAndUpdate( { session: oldSessionId }, { session: req.sessionID, lastSeen: new Date() } ) next() }) } else { next() } })
5. 跨服务器重启保持用户连接
因为我们把会话存在了MongoDB里,而不是服务器内存,所以服务器重启后,会话数据依然存在,用户不需要重新登录。
最后要注意的点
- 密码一定要加密存储!不要像示例里那样明文存,用
bcrypt或者其他加密库处理。 connect-mongo的配置可以根据你的MongoDB版本调整,比如如果用MongoDB 6.0+,可能需要配置mongoOptions。- 会话的
secret一定要用强随机字符串,不要硬编码在代码里,用环境变量管理。
内容的提问来源于stack exchange,提问作者nowox
相关产品推荐
相关产品推荐

