Passport.js 会话管理 #

什么是会话? #

会话(Session)是一种在服务器端存储用户状态的机制。由于 HTTP 协议是无状态的,每次请求都是独立的,会话允许我们在多个请求之间保持用户状态。

text
┌─────────────────────────────────────────────────────────────┐
│                      会话工作原理                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   第一次请求                                                 │
│   ┌─────────┐                    ┌─────────┐               │
│   │ 客户端  │ ──── 请求 ────────>│ 服务器  │               │
│   └─────────┘                    └────┬────┘               │
│        ▲                              │                     │
│        │                              │ 创建会话            │
│        │                              ▼                     │
│        │                         ┌─────────┐               │
│        └───── Set-Cookie ────────│ Session │               │
│              session_id=abc123   │ Store   │               │
│                                      │                     │
│   后续请求                            │                     │
│   ┌─────────┐                        │                     │
│   │ 客户端  │ ──── Cookie ─────────>│ 服务器 │             │
│   │session_id=abc123│               └────┬────┘           │
│   └─────────┘                           │                  │
│        ▲                                │ 查找会话         │
│        │                                ▼                  │
│        │                           ┌─────────┐            │
│        └───────────────────────────│ Session │            │
│                                      │ Store   │            │
│                                      └─────────┘            │
└─────────────────────────────────────────────────────────────┘

Passport.js 会话机制 #

核心概念 #

Passport.js 使用会话来保持用户的登录状态。主要涉及两个关键函数:

  1. 序列化(serializeUser):将用户对象转换为可存储的形式
  2. 反序列化(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 }
}));

会话安全 #

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