乐闻世界logo
搜索文章和话题

如何使用 Cookie 实现"记住我"功能?需要注意哪些安全问题?

3月6日 21:24

使用 Cookie 实现"记住我"功能需要考虑安全性、用户体验和持久化存储等多个方面。

"记住我"功能原理

  • 在用户登录成功后,生成一个长期有效的认证令牌
  • 将令牌存储在持久 Cookie 中
  • 用户下次访问时,自动使用 Cookie 中的令牌完成登录

实现方案

方案 1:持久 Session Cookie

javascript
// 服务器端设置(Node.js Express) function setRememberMeCookie(res, token, rememberMe) { const options = { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', path: '/' }; if (rememberMe) { // 长期 Cookie:30天 options.maxAge = 30 * 24 * 60 * 60; } else { // 会话 Cookie:浏览器关闭时删除 options.maxAge = null; } res.cookie('authToken', token, options); }

方案 2:双令牌机制

javascript
// 生成访问令牌和刷新令牌 function generateTokens(userId) { const accessToken = jwt.sign( { userId }, process.env.JWT_SECRET, { expiresIn: '15m' } // 短期有效 ); const refreshToken = crypto.randomBytes(32).toString('hex'); // 存储刷新令牌到数据库 db.saveRefreshToken(userId, refreshToken, { expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) }); return { accessToken, refreshToken }; } // 设置 Cookie function setAuthCookies(res, tokens, rememberMe) { // 访问令牌:短期,HttpOnly res.cookie('accessToken', tokens.accessToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 15 * 60 // 15分钟 }); // 刷新令牌:长期,HttpOnly if (rememberMe) { res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, secure: true, sameSite: 'strict', maxAge: 30 * 24 * 60 * 60 // 30天 }); } }

安全最佳实践

  1. 令牌生成
javascript
// 使用加密安全的随机数生成器 const crypto = require('crypto'); function generateSecureToken() { return crypto.randomBytes(32).toString('hex'); }
  1. 令牌存储
javascript
// 数据库存储方案 const refreshTokenSchema = new Schema({ userId: { type: ObjectId, required: true }, token: { type: String, required: true, unique: true }, createdAt: { type: Date, default: Date.now }, expiresAt: { type: Date, required: true }, lastUsedAt: { type: Date, default: Date.now }, userAgent: String, ipAddress: String });
  1. 令牌验证
javascript
async function verifyRefreshToken(token, req) { const record = await db.findRefreshToken(token); if (!record) { throw new Error('Invalid token'); } if (record.expiresAt < new Date()) { await db.deleteRefreshToken(token); throw new Error('Token expired'); } // 可选:验证 User-Agent 和 IP if (record.userAgent !== req.headers['user-agent']) { await db.deleteRefreshToken(token); throw new Error('Token compromised'); } // 更新最后使用时间 await db.updateRefreshToken(token, { lastUsedAt: new Date() }); return record.userId; }

用户体验优化

  1. 登录表单
html
<form id="loginForm"> <input type="text" name="username" placeholder="用户名" required> <input type="password" name="password" placeholder="密码" required> <label> <input type="checkbox" name="rememberMe"> 记住我(30天内自动登录) </label> <button type="submit">登录</button> </form>
  1. 自动登录流程
javascript
// 页面加载时检查 Cookie async function checkAutoLogin() { const refreshToken = getCookie('refreshToken'); if (refreshToken) { try { const response = await fetch('/api/auth/refresh', { method: 'POST', credentials: 'include' }); if (response.ok) { const { accessToken } = await response.json(); localStorage.setItem('accessToken', accessToken); // 跳转到首页 window.location.href = '/dashboard'; } } catch (error) { console.error('Auto login failed:', error); } } }

安全增强措施

  1. 令牌轮换
javascript
// 每次使用刷新令牌时生成新的刷新令牌 async function rotateRefreshToken(oldToken) { const userId = await verifyRefreshToken(oldToken, req); // 删除旧令牌 await db.deleteRefreshToken(oldToken); // 生成新令牌 const newToken = generateSecureToken(); await db.saveRefreshToken(userId, newToken, { expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) }); return newToken; }
  1. 撤销机制
javascript
// 用户登出时撤销所有令牌 async function logoutAllDevices(userId) { await db.deleteAllRefreshTokens(userId); res.clearCookie('accessToken'); res.clearCookie('refreshToken'); }
  1. 设备管理
javascript
// 显示已登录设备列表 async function getActiveDevices(userId) { const tokens = await db.findRefreshTokensByUser(userId); return tokens.map(token => ({ device: parseUserAgent(token.userAgent), lastUsed: token.lastUsedAt, current: token.userAgent === req.headers['user-agent'] })); }
标签:Cookie