NodeJS+Passport+MongoDB栈如何实现仅保留用户最近N个会话并登出其余
实现方案
你当前使用cookie-session将会话全量存储在客户端,服务端没有统一的会话管理入口,确实无法直接实现多会话管控,可从以下两种方案中选择适配:
方案一:切换为服务端会话存储(推荐)
将cookie-session替换为express-session + connect-mongo,将会话存储在MongoDB中,可直接查询、批量操作用户的所有会话。
步骤1:安装依赖
npm install express-session connect-mongo
步骤2:修改会话配置
替换原有的cookie-session配置:
const session = require('express-session'); const MongoStore = require('connect-mongo'); app.use(cookieParser("something secret")); app.use( session({ maxAge: 90 * 24 * 60 * 60 * 1000, secret: config.secret, signed: true, resave: false, saveUninitialized: false, httpOnly: true, secure: true, // 新增MongoDB会话存储配置,url替换为你的MongoDB连接地址 store: MongoStore.create({ mongoUrl: 'mongodb://localhost:27017/你的数据库名', collectionName: 'sessions', // 会话集合名,默认就是sessions autoRemove: 'native' // 自动清理过期会话 }) }) );
步骤3:修改登录逻辑,新增会话清理规则
替换原有的登录路由,使用Passport自定义回调处理登录后的会话清理:
router.post("/login", function(req, res, next) { passport.authenticate("user-otp", async function(err, user, info) { if (err) return next(err); if (!user) return res.redirect("back"); // 手动登录用户,创建会话 req.logIn(user, async function(err) { if (err) return next(err); // 给当前会话打登录时间标记 req.session.loginAt = new Date(); await req.session.save(); // 定义最多保留的会话数N,可改成从用户模型字段读取 const MAX_SESSIONS = 3; const mongoose = require('mongoose'); const Session = mongoose.connection.collection('sessions'); // 查询该用户所有会话,按登录时间倒序排列 const userSessions = await Session.find({ "session.passport.user": user._id.toString() }).sort({"session.loginAt": -1}).toArray(); // 保留前N个,删除剩余旧会话 if (userSessions.length > MAX_SESSIONS) { const expiredSessionIds = userSessions.slice(MAX_SESSIONS).map(s => s._id); await Session.deleteMany({_id: {$in: expiredSessionIds}}); } return res.redirect("/dashboard"); }); })(req, res, next); });
方案二:无需更换会话存储,自行维护有效会话列表
如果不想改动现有cookie-session架构,可通过在用户模型中维护有效会话标识的方式实现管控。
步骤1:扩展用户模型
const UserSchema = new Schema({ // 原有字段 activeSessions: [{ sessionId: String, loginAt: { type: Date, default: Date.now }, userAgent: String // 可选,存储设备信息用于展示 }] });
步骤2:修改登录逻辑
登录成功后生成唯一会话标识,更新用户的有效会话列表:
const { v4: uuidv4 } = require('uuid'); // 需先安装uuid包 passport.use('user-otp', new CustomStrategy( async function(req, done) { const user = await User.findOne({email: req.body.email}); const check = await user.validateOtp(req.body.otp); if (!check) return done(null, false); // 生成唯一会话ID const sessionId = uuidv4(); // 新增当前会话到列表 user.activeSessions.push({ sessionId, loginAt: new Date(), userAgent: req.get('User-Agent') }); // 保留最近N个会话 const MAX_SESSIONS = 3; if (user.activeSessions.length > MAX_SESSIONS) { user.activeSessions.sort((a,b) => b.loginAt - a.loginAt); user.activeSessions = user.activeSessions.slice(0, MAX_SESSIONS); } await user.save(); // 将会话ID存到客户端cookie req.session.userSessionId = sessionId; return done(null, user); } ));
步骤3:新增全局校验中间件
所有请求前校验当前会话是否在用户的有效列表中:
app.use(async (req, res, next) => { // 放行登录等无需认证的接口 if (req.path === '/login' || !req.isAuthenticated()) return next(); const user = await User.findById(req.user._id); const validSession = user.activeSessions.find(s => s.sessionId === req.session.userSessionId); if (!validSession) { // 会话不在有效列表中,强制登出 return req.logout(() => res.redirect('/login')); } next(); });
内容的提问来源于stack exchange,提问作者ginjaemocoes
相关产品推荐
相关产品推荐

