Node.js中Access/Refresh Token实现合理性及安全优化咨询
关于Access/Refresh Token机制的实现分析与优化方案
一、当前实现的合理性分析
你的基础逻辑方向是对的:优先校验Access Token,失效后用Refresh Token刷新双Token,这个流程符合标准的Token刷新模式。但存在几个明显的问题:
- 回调地狱问题:嵌套两层
jwt.verify回调,代码可读性和维护性差,后续扩展逻辑会更混乱。 - 无状态Refresh Token的风险:JWT本身是无状态的,你当前只验证Refresh Token的签名和有效期,没有在服务器端记录Token的状态。如果Refresh Token被盗,攻击者可以一直用它刷新新的Token,直到30天过期。
- Cookie安全属性缺失:代码里没体现设置Cookie的
HttpOnly、Secure、SameSite属性,这会导致XSS或CSRF攻击风险——比如XSS脚本可以窃取非HttpOnly的Cookie,CSRF攻击可以冒用用户的Cookie发起请求。
二、Refresh Token被盗后的机制风险
这种情况下机制不是冗余,而是存在严重安全漏洞:被盗的Refresh Token在有效期内(30天)可以无限次用来获取新的Access Token和Refresh Token,攻击者能长期冒充用户身份。你的当前实现没有任何手段提前失效被盗的Refresh Token,这是很大的安全隐患。
三、Refresh Token的存储位置选择
Cookie是最优选择,但必须配置安全属性:
- 必须设置
HttpOnly: true:禁止前端JS读取Cookie,从根源上防止XSS窃取。 - 必须设置
Secure: true:仅在HTTPS连接下才发送Cookie,避免明文传输被窃听。 - 设置
SameSite: 'Strict'或'Lax':限制Cookie跨域发送,防范CSRF攻击。 - 绝对不要存在
localStorage或sessionStorage:这些存储可以被前端JS直接读取,XSS脚本轻易就能窃取Token。
四、兼顾便捷性与安全性的正确方案
1. 服务器端存储Refresh Token的哈希值
- 登录生成Refresh Token后,不要直接存储明文,而是存储它的哈希值(比如用bcrypt哈希)到数据库,关联用户ID、过期时间、设备标识(可选)。
- 刷新Token时,先验证Refresh Token的签名和有效期,再去数据库查询对应的哈希值是否存在且有效。
- 刷新成功后,删除旧的Refresh Token哈希,存储新的Refresh Token哈希——这样旧的Refresh Token立刻失效,即使被盗也无法再使用。
2. 优化Token刷新逻辑
改用async/await重构验证代码,避免回调地狱:
exports.verifyAccessToken = (redirect) => async (req, res, next) => { const accessToken = req.cookies.accessToken; const refreshToken = req.cookies.refreshToken; log.debug("verifying, cookies:", req.cookies); if (!accessToken) { return redirect ? res.redirect("/login") : res.status(401).json({ message: 'Verification Error: Access Token missing' }); } try { const decoded = jwt.verify(accessToken, process.env.ACCESS_TOKEN_SECRET); req.user = decoded; return next(); } catch (accessErr) { if (!refreshToken) { return redirect ? res.redirect("/login") : res.status(400).json({ message: 'Verification Error: Access Token invalid/expired, Refresh Token missing' }); } try { const decodedRefresh = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET); // 从数据库查询该Refresh Token的哈希是否存在且有效 const storedSession = await db.query('SELECT refresh_hash FROM user_sessions WHERE user_id = ? AND expires_at > NOW()', [decodedRefresh.userId]); if (!storedSession.length || !bcrypt.compareSync(refreshToken, storedSession[0].refresh_hash)) { return redirect ? res.redirect("/login") : res.status(403).json({ message: 'Verification Error: Refresh Token invalid/revoked' }); } // 生成新的双Token const newAccessToken = helper.generateNewAccessToken(decodedRefresh.email); const newRefreshToken = helper.generateNewRefreshToken(decodedRefresh.email); // 存储新的Refresh Token哈希,删除旧的 await db.query('DELETE FROM user_sessions WHERE user_id = ?', [decodedRefresh.userId]); await db.query('INSERT INTO user_sessions (user_id, refresh_hash, expires_at) VALUES (?, ?, ?)', [ decodedRefresh.userId, bcrypt.hashSync(newRefreshToken, 10), new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) ]); // 设置新的Cookie,添加安全属性 res.cookie('accessToken', newAccessToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'Strict', maxAge: 60 * 60 * 1000 // 1小时 }); res.cookie('refreshToken', newRefreshToken, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'Strict', maxAge: 30 * 24 * 60 * 60 * 1000 // 30天 }); req.user = decodedRefresh; next(); } catch (refreshErr) { return redirect ? res.redirect("/login") : res.status(403).json({ message: 'Verification Error: Refresh Token invalid/expired' }); } } };
3. 其他安全优化措施
- 缩短Refresh Token有效期:比如从30天缩短到7天,同时提供“记住我”选项,用户勾选后可以延长到30天——平衡安全性和便捷性。
- 设备绑定:生成Token时记录用户的设备信息(比如User-Agent哈希),刷新时校验设备信息,避免跨设备盗用。
- 主动失效机制:提供登出接口,删除数据库中对应的Refresh Token哈希;用户修改密码、账号异常时,批量失效该用户的所有Refresh Token。
- 敏感操作二次验证:比如修改密码、支付等操作,强制要求用户输入密码或验证码,即使Token有效也需要额外验证。
内容的提问来源于stack exchange,提问作者Aran Bins
相关产品推荐
相关产品推荐

