Skip to content
On this page

会话管理

会话管理是 Web 应用安全的关键组成部分,它涉及如何创建、维护和销毁用户会话,确保只有合法用户可以访问受限资源。

会话管理基础

什么是会话

会话是在一段时间内,服务器与客户端之间的一系列交互。在 Web 应用中,由于 HTTP 协议本身是无状态的,会话管理允许服务器在多个请求之间"记住"用户。

会话生命周期

  1. 创建 - 用户首次访问或登录时创建
  2. 维护 - 在用户活动期间保持会话有效性
  3. 更新 - 根据用户活动更新会话状态
  4. 销毁 - 用户登出或会话过期时清除会话

会话标识符

生成安全的会话 ID

会话 ID 必须是:

  • 高熵(难以预测)
  • 足够长(防止暴力破解)
  • 全球唯一(防止冲突)
javascript
const crypto = require('crypto');

// 生成安全的会话 ID
function generateSessionId() {
  return crypto.randomBytes(32).toString('hex');
}

// 或使用 uuid
const { v4: uuidv4 } = require('uuid');
function generateSecureSessionId() {
  return uuidv4();
}

会话 ID 存储

javascript
// 使用 Express Session
const session = require('express-session');
const MongoStore = require('connect-mongo');

app.use(session({
  genid: (req) => {
    // 生成自定义会话 ID
    return crypto.randomBytes(32).toString('hex');
  },
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  store: MongoStore.create({
    mongoUrl: process.env.MONGODB_URI
  }),
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    maxAge: 24 * 60 * 60 * 1000 // 24小时
  }
}));

会话存储选项

1. 内存存储

javascript
// 适合开发环境
const MemoryStore = require('memorystore')(session);

app.use(session({
  store: new MemoryStore({
    checkPeriod: 86400000 // 清理过期会话的周期
  }),
  secret: 'keyboard cat',
  cookie: { maxAge: 86400000 } // 24小时
}));

2. Redis 存储

javascript
const redis = require('redis');
const RedisStore = require('connect-redis')(session);

// 创建 Redis 客户端
const redisClient = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT,
  password: process.env.REDIS_PASSWORD
});

app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: true, // 仅通过 HTTPS
    httpOnly: true, // 防止 XSS
    maxAge: 1000 * 60 * 60 * 24 // 24小时
  }
}));

3. 数据库存储

javascript
// 使用 Sequelize 存储会话
const SessionStore = require('express-session-sequelize')(session.Store);

app.use(session({
  store: new SessionStore({
    db: sequelize // Sequelize 实例
  }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false
}));

会话安全配置

javascript
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    // 仅通过 HTTPS 传输
    secure: process.env.NODE_ENV === 'production',
    
    // 防止通过 JavaScript 访问
    httpOnly: true,
    
    // 防止 CSRF 攻击
    sameSite: 'strict',
    
    // 会话有效期
    maxAge: 24 * 60 * 60 * 1000 // 24小时
  }
}));

会话固定防护

javascript
// 登录成功后生成新的会话 ID
app.post('/login', (req, res) => {
  // 验证用户凭据
  if (isValidCredentials(req.body)) {
    // 在新会话中存储用户信息
    req.session.regenerate((err) => {
      if (err) {
        return res.status(500).json({ error: 'Session regeneration failed' });
      }
      
      // 存储用户信息
      req.session.userId = authenticatedUser.id;
      req.session.username = authenticatedUser.username;
      req.session.authenticated = true;
      
      res.json({ success: true });
    });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

会话超时管理

绝对超时

javascript
// 绝对超时中间件
function absoluteTimeout(req, res, next) {
  if (req.session.lastActivity) {
    const now = Date.now();
    const maxSessionTime = 8 * 60 * 60 * 1000; // 8小时
    
    if (now - req.session.lastActivity > maxSessionTime) {
      req.session.destroy(() => {});
      return res.status(401).json({ error: 'Session expired' });
    }
  }
  
  req.session.lastActivity = Date.now();
  next();
}

非活动超时

javascript
// 非活动超时中间件
function idleTimeout(req, res, next) {
  if (req.session.lastActivity) {
    const now = Date.now();
    const idleTimeout = 30 * 60 * 1000; // 30分钟
    
    if (now - req.session.lastActivity > idleTimeout) {
      req.session.destroy(() => {});
      return res.status(401).json({ error: 'Session timed out due to inactivity' });
    }
  }
  
  req.session.lastActivity = Date.now();
  next();
}

会话验证和中间件

会话验证中间件

javascript
// 验证会话是否有效
function validateSession(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'No active session' });
  }
  
  // 额外验证(如检查用户是否仍然存在)
  User.findById(req.session.userId)
    .then(user => {
      if (!user) {
        req.session.destroy(() => {});
        return res.status(401).json({ error: 'User no longer exists' });
      }
      
      // 更新最后活动时间
      req.session.lastActivity = Date.now();
      next();
    })
    .catch(err => {
      console.error('Session validation error:', err);
      res.status(500).json({ error: 'Session validation failed' });
    });
}

// 使用中间件
app.get('/protected-route', validateSession, (req, res) => {
  res.json({ message: 'Protected content', user: req.session.username });
});

管理员会话验证

javascript
function requireAdmin(req, res, next) {
  if (!req.session.userId || req.session.role !== 'admin') {
    return res.status(403).json({ error: 'Admin access required' });
  }
  
  // 验证管理员权限未过期
  if (req.session.adminPrivilegesExpiry && 
      Date.now() > req.session.adminPrivilegesExpiry) {
    req.session.role = 'user'; // 降级权限
    return res.status(403).json({ error: 'Admin privileges expired' });
  }
  
  next();
}

会话清理和维护

定期清理过期会话

javascript
// 清理会话的定时任务
async function cleanupExpiredSessions() {
  try {
    // 对于数据库存储的会话
    await Session.destroy({
      where: {
        expires: {
          [Op.lt]: new Date()
        }
      }
    });
    
    console.log('Expired sessions cleaned up');
  } catch (error) {
    console.error('Error cleaning up sessions:', error);
  }
}

// 每天凌晨 2 点执行清理
const cron = require('node-cron');
cron.schedule('0 2 * * *', cleanupExpiredSessions);

会话统计和监控

javascript
// 会话统计中间件
function sessionStats(req, res, next) {
  // 记录活跃会话数
  req.session.visitCount = (req.session.visitCount || 0) + 1;
  req.session.lastVisit = new Date().toISOString();
  
  // 可以将会话数据发送到监控系统
  if (process.env.NODE_ENV === 'production') {
    // 发送到监控服务
    analytics.trackSessionActivity({
      userId: req.session.userId,
      visitCount: req.session.visitCount,
      lastVisit: req.session.lastVisit
    });
  }
  
  next();
}

多设备会话管理

限制并发会话数

javascript
// 会话管理器
class SessionManager {
  constructor() {
    this.activeSessions = new Map(); // userId -> [sessionId1, sessionId2, ...]
    this.maxConcurrentSessions = 3;
  }
  
  async createSession(userId) {
    const existingSessions = this.activeSessions.get(userId) || [];
    
    if (existingSessions.length >= this.maxConcurrentSessions) {
      // 移除最旧的会话
      const oldestSessionId = existingSessions.shift();
      await this.destroySession(oldestSessionId);
    }
    
    const newSessionId = generateSessionId();
    existingSessions.push(newSessionId);
    this.activeSessions.set(userId, existingSessions);
    
    return newSessionId;
  }
  
  async destroySession(sessionId) {
    // 从存储中删除会话
    // 从内存中删除会话
  }
}

设备特定的会话管理

javascript
// 基于设备指纹的会话管理
function generateDeviceFingerprint(req) {
  const userAgent = req.headers['user-agent'];
  const ip = req.ip;
  const acceptLanguage = req.headers['accept-language'];
  const acceptEncoding = req.headers['accept-encoding'];
  
  const fingerprintData = `${userAgent}-${ip}-${acceptLanguage}-${acceptEncoding}`;
  return crypto.createHash('sha256').update(fingerprintData).digest('hex');
}

app.post('/login', (req, res) => {
  const deviceFingerprint = generateDeviceFingerprint(req);
  
  req.session.regenerate(() => {
    req.session.userId = user.id;
    req.session.deviceFingerprint = deviceFingerprint;
    req.session.loginTime = Date.now();
    
    res.json({ success: true });
  });
});

// 验证设备指纹
function verifyDeviceFingerprint(req, res, next) {
  if (req.session.deviceFingerprint) {
    const currentFingerprint = generateDeviceFingerprint(req);
    if (req.session.deviceFingerprint !== currentFingerprint) {
      // 设备发生变化,要求重新验证
      req.session.destroy(() => {});
      return res.status(401).json({ error: 'Device changed, please login again' });
    }
  }
  next();
}

登出和会话终止

安全登出

javascript
app.post('/logout', (req, res) => {
  // 保存一些登出信息
  const logoutInfo = {
    userId: req.session.userId,
    logoutTime: new Date(),
    sessionId: req.sessionID
  };
  
  // 销毁会话
  req.session.destroy((err) => {
    if (err) {
      console.error('Error destroying session:', err);
      return res.status(500).json({ error: 'Logout failed' });
    }
    
    // 清除会话 Cookie
    res.clearCookie('connect.sid');
    
    // 记录登出事件
    auditLog.logout(logoutInfo);
    
    res.json({ success: true, message: 'Logged out successfully' });
  });
});

全部登出(登出所有设备)

javascript
app.post('/logout-all', authenticateJWT, async (req, res) => {
  try {
    // 从数据库删除用户的所有会话
    await Session.destroy({
      where: { userId: req.user.userId }
    });
    
    // 也可以在内存中维护一个会话黑名单
    addToSessionBlacklist(req.user.userId);
    
    res.json({ success: true, message: 'Logged out from all devices' });
  } catch (error) {
    console.error('Error logging out all sessions:', error);
    res.status(500).json({ error: 'Logout failed' });
  }
});

会话安全最佳实践

1. 定期轮换会话 ID

javascript
// 定期轮换会话 ID
function periodicSessionRotation(req, res, next) {
  const now = Date.now();
  const lastRotation = req.session.lastRotation || 0;
  const rotationInterval = 30 * 60 * 1000; // 30分钟
  
  if (now - lastRotation > rotationInterval) {
    req.session.regenerate((err) => {
      if (err) {
        console.error('Session regeneration error:', err);
      } else {
        req.session.lastRotation = Date.now();
      }
      next();
    });
  } else {
    next();
  }
}

2. 会话劫持检测

javascript
// 检测 IP 地址变化
function detectSessionHijacking(req, res, next) {
  if (req.session.originalIp && req.session.originalIp !== req.ip) {
    // IP 地址发生变化,可能是会话劫持
    req.session.destroy(() => {});
    return res.status(401).json({ error: 'Potential session hijacking detected' });
  }
  
  // 记录原始 IP(首次设置)
  if (!req.session.originalIp) {
    req.session.originalIp = req.ip;
  }
  
  next();
}

3. 会话数据加密

javascript
const crypto = require('crypto');

// 加密会话数据
function encryptSessionData(data, key) {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipher('aes-256-cbc', key);
  let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
  encrypted += cipher.final('hex');
  
  return {
    data: encrypted,
    iv: iv.toString('hex')
  };
}

// 解密会话数据
function decryptSessionData(encryptedData, key) {
  const decipher = crypto.createDecipher('aes-256-cbc', key);
  let decrypted = decipher.update(encryptedData.data, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  
  return JSON.parse(decrypted);
}

总结

有效的会话管理是 Web 应用安全的关键。关键要点包括:

  1. 使用安全的会话 ID 生成方法
  2. 正确配置 Cookie 安全属性
  3. 实施适当的超时机制
  4. 定期清理过期会话
  5. 防护会话固定和劫持攻击
  6. 实现安全的登出机制
  7. 监控和审计会话活动