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

如何借助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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 07:16:38