You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 05:20:27