Node.js Express中用户改密码后如何强制过期JWT重置令牌?
需求说明
我正在开发基于Node.js+Express的项目,需向用户发送含重置密码链接的邮件,链接中的JWT令牌默认15分钟后过期。希望实现:用户修改密码后强制使旧重置令牌失效,避免多次重复改密。
当前重置密码请求接口代码:
exports.resetPasswordRequest = async (req, res) =>{ res.status(200).json({status: 'success', message: "Your request was successfully submitted" }); if(req.body.email){ const email = req.body.email; const user = await User.findOne({ where: { email: email } }); if(user){ if(user.acc_status === 1){ //Create Token const payload = { email: user.email, id: user.id } const token = jwt.sign(payload, config.secret,{ expiresIn: config.jwtResetPasswordExpiration, }); const link = FE_LINK + '/auth/reset-password/'+ token + '?source=email' sendPasswordResetEmail(email, link) } } } }
修改密码功能代码:
exports.updatePassword = async (req, res) =>{ const {id, oldPassword, newPassword, resetPasswordToken} = req.body //Check if token is valid const secret = config.secret jwt.verify(req.body.resetPasswordToken, secret, (err, decoded)=>{ if (err) { return res.status(200).json({ message: 'INVALID' }); } let user_id = decoded.id User.findOne({ where: { id: user_id } }).then(async (user) => { if(oldPassword){ if(bcrypt.compareSync(oldPassword,user.password) == false){ return res.status(200).json({ error:"true",message: 'Your current password is wrong' }); } } user.password = bcrypt.hashSync(newPassword, 8) user.password_changedAt = new Date() user.new_password_required = false await user.save(); return res.status(200).json({ error:"false",message: 'You changed your password successfully' }); }).catch(err => { res.status(500).send({ error:"true",message: err.message }); }); }) };
实现方案
要实现旧重置令牌失效,核心思路是通过时间校验或令牌存储校验,确保只有未过期且未被替换的令牌能完成密码修改。以下是两种可行方案:
方案一:利用密码修改时间校验令牌(无需改数据库)
1. 重置令牌Payload添加签发时间
生成令牌时,在Payload中显式加入签发时间,后续验证时对比该时间与密码修改时间:
exports.resetPasswordRequest = async (req, res) => { res.status(200).json({status: 'success', message: "Your request was successfully submitted" }); if(!req.body.email) return; const email = req.body.email; const user = await User.findOne({ where: { email: email } }); if(user && user.acc_status === 1){ const payload = { email: user.email, id: user.id, issuedAt: Date.now() // 添加令牌签发时间戳 } const token = jwt.sign(payload, config.secret,{ expiresIn: config.jwtResetPasswordExpiration, }); const link = FE_LINK + '/auth/reset-password/'+ token + '?source=email' sendPasswordResetEmail(email, link) } }
2. 修改密码时增加时间校验
验证JWT后,检查令牌签发时间是否早于密码修改时间,若令牌是在密码修改后生成的则判定无效:
exports.updatePassword = async (req, res) => { const { newPassword, resetPasswordToken } = req.body; const secret = config.secret; try { const decoded = jwt.verify(resetPasswordToken, secret); const user = await User.findOne({ where: { id: decoded.id } }); if(!user) return res.status(404).json({ error:"true", message: '用户不存在' }); // 核心校验:令牌签发时间晚于密码修改时间则无效 if(user.password_changedAt && decoded.issuedAt < user.password_changedAt.getTime()){ return res.status(200).json({ message: 'INVALID' }); } // 旧密码验证逻辑(按需保留,重置密码场景可移除) if(req.body.oldPassword){ if(!bcrypt.compareSync(req.body.oldPassword, user.password)){ return res.status(200).json({ error:"true", message: '当前密码错误' }); } } user.password = bcrypt.hashSync(newPassword, 8); user.password_changedAt = new Date(); user.new_password_required = false; await user.save(); return res.status(200).json({ error:"false", message: '密码修改成功' }); } catch(err) { if(err.name === 'JsonWebTokenError'){ return res.status(200).json({ message: 'INVALID' }); } return res.status(500).send({ error:"true", message: err.message }); } };
方案二:存储当前有效令牌(更严谨,需改数据库)
1. 用户表新增字段
在User模型中添加两个字段(以Sequelize为例):
resetPasswordToken: { type: DataTypes.STRING, allowNull: true }, resetTokenExpires: { type: DataTypes.DATE, allowNull: true }
2. 重置密码时更新令牌存储
生成新令牌后,覆盖用户记录中的旧令牌,并设置过期时间:
exports.resetPasswordRequest = async (req, res) => { res.status(200).json({status: 'success', message: "Your request was successfully submitted" }); if(!req.body.email) return; const email = req.body.email; const user = await User.findOne({ where: { email: email } }); if(user && user.acc_status === 1){ const payload = { email: user.email, id: user.id } const token = jwt.sign(payload, config.secret,{ expiresIn: config.jwtResetPasswordExpiration, }); // 计算令牌过期时间 const expires = new Date(); expires.setMinutes(expires.getMinutes() + parseInt(config.jwtResetPasswordExpiration.split('m')[0])); // 更新用户的重置令牌和过期时间 await user.update({ resetPasswordToken: token, resetTokenExpires: expires }); const link = FE_LINK + '/auth/reset-password/'+ token + '?source=email' sendPasswordResetEmail(email, link) } }
3. 修改密码时校验令牌一致性
验证JWT的同时,检查请求令牌是否与数据库中存储的一致,且未过期:
const { Op } = require('sequelize'); // 引入操作符 exports.updatePassword = async (req, res) => { const { newPassword, resetPasswordToken } = req.body; const secret = config.secret; try { const decoded = jwt.verify(resetPasswordToken, secret); // 同时校验令牌一致性和过期时间 const user = await User.findOne({ where: { id: decoded.id, resetPasswordToken: resetPasswordToken, resetTokenExpires: { [Op.gt]: new Date() } } }); if(!user) return res.status(200).json({ message: 'INVALID' }); // 旧密码验证逻辑(按需保留) if(req.body.oldPassword){ if(!bcrypt.compareSync(req.body.oldPassword, user.password)){ return res.status(200).json({ error:"true", message: '当前密码错误' }); } } user.password = bcrypt.hashSync(newPassword, 8); user.password_changedAt = new Date(); user.new_password_required = false; // 清空令牌使其失效 user.resetPasswordToken = null; user.resetTokenExpires = null; await user.save(); return res.status(200).json({ error:"false", message: '密码修改成功' }); } catch(err) { if(err.name === 'JsonWebTokenError'){ return res.status(200).json({ message: 'INVALID' }); } return res.status(500).send({ error:"true", message: err.message }); } };
方案对比
- 方案一:无需修改数据库,实现简单,但用户多次请求重置密码时,旧令牌仍会在15分钟内有效(直到密码修改)。
- 方案二:更严谨,每次请求重置密码都会覆盖旧令牌,旧令牌立即失效,但需要调整数据库结构。
内容的提问来源于stack exchange,提问作者nargyriou
相关产品推荐
相关产品推荐

