Appearance
会话管理
会话管理是 Web 应用安全的关键组成部分,它涉及如何创建、维护和销毁用户会话,确保只有合法用户可以访问受限资源。
会话管理基础
什么是会话
会话是在一段时间内,服务器与客户端之间的一系列交互。在 Web 应用中,由于 HTTP 协议本身是无状态的,会话管理允许服务器在多个请求之间"记住"用户。
会话生命周期
- 创建 - 用户首次访问或登录时创建
- 维护 - 在用户活动期间保持会话有效性
- 更新 - 根据用户活动更新会话状态
- 销毁 - 用户登出或会话过期时清除会话
会话标识符
生成安全的会话 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
}));
会话安全配置
安全的 Cookie 设置
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 应用安全的关键。关键要点包括:
- 使用安全的会话 ID 生成方法
- 正确配置 Cookie 安全属性
- 实施适当的超时机制
- 定期清理过期会话
- 防护会话固定和劫持攻击
- 实现安全的登出机制
- 监控和审计会话活动