You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.04 18:51:02