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

基于Node.js+MySQL的密码找回开发求助:bcrypt令牌链路实现

密码找回模块完整实现指南

我来一步步帮你搞定这个密码找回流程,结合你用到的Node.js、MySQL和那些npm包,分模块拆解核心步骤:

一、先理清核心逻辑(关键误区提醒)

首先要明确:bcrypt是用来哈希敏感数据(比如密码、令牌的哈希值)的,不能直接把bcrypt哈希后的字符串当URL令牌用(太长且没必要)。正确的流程是:

  1. 用户提交找回请求→生成随机原始令牌
  2. 把令牌的bcrypt哈希值+过期时间存到MySQL用户表
  3. 将原始令牌拼到重置链接里,用nodemailer发给用户
  4. 用户点击链接→后端验证令牌有效性(对比哈希+检查过期)
  5. 验证通过后,把用户非敏感数据传递到重置表单页面
  6. 用户提交新密码→再次验证令牌后更新密码,清空令牌记录

二、前置准备:MySQL用户表修改

先给你的用户表加两个字段,用来存储重置令牌和过期时间:

ALTER TABLE users 
ADD COLUMN reset_token VARCHAR(255) NULL,
ADD COLUMN reset_expiry DATETIME NULL;

三、生成安全令牌并发送邮件

1. 生成随机令牌 + 哈希存储

用Node.js内置的crypto模块生成安全随机令牌,再用bcrypt哈希后存入数据库:

const crypto = require('crypto');
const bcrypt = require('bcrypt');
const mysql = require('mysql');

// 假设你已创建MySQL连接池
const db = mysql.createPool({
  host: '你的数据库地址',
  user: '数据库用户名',
  password: '数据库密码',
  database: '你的数据库名'
});

// 生成找回令牌的异步函数
async function generateResetToken(email) {
  // 生成16字节随机令牌,转十六进制字符串(短且安全)
  const rawToken = crypto.randomBytes(16).toString('hex');
  // 哈希令牌(bcrypt盐轮次设为10足够安全)
  const hashedToken = await bcrypt.hash(rawToken, 10);
  // 设置1小时后过期
  const expiry = new Date(Date.now() + 3600000);

  // 更新用户表的令牌和过期时间
  return new Promise((resolve, reject) => {
    db.query(
      'UPDATE users SET reset_token = ?, reset_expiry = ? WHERE email = ?',
      [hashedToken, expiry, email],
      (err, result) => {
        if (err) reject(err);
        // 返回原始令牌,用来拼接重置链接
        resolve(rawToken);
      }
    );
  });
}

2. 用nodemailer发送带重置链接的邮件

const nodemailer = require('nodemailer');

// 配置nodemailer传输器(示例用Gmail,可替换为你的企业邮箱)
const transporter = nodemailer.createTransport({
  service: 'Gmail',
  auth: {
    user: '你的发送邮箱@example.com',
    pass: '邮箱授权码(不是登录密码)'
  }
});

// 发送找回邮件的异步函数
async function sendResetEmail(email, rawToken) {
  const resetUrl = `http://你的域名.com/reset-password?token=${rawToken}`;
  const mailOptions = {
    from: '你的应用名 <no-reply@example.com>',
    to: email,
    subject: '重置你的密码',
    html: `
      <p>点击下方链接重置密码:</p>
      <a href="${resetUrl}">${resetUrl}</a>
      <p>链接1小时内有效,请勿转发给他人</p>
    `
  };

  return transporter.sendMail(mailOptions);
}

// 处理用户提交找回请求的路由
app.post('/forgot-password', async (req, res) => {
  try {
    const { email } = req.body;
    // 可选:先检查用户是否存在,避免无效请求
    const [user] = await db.query('SELECT * FROM users WHERE email = ?', [email]);
    if (!user.length) return res.status(404).send('该邮箱未注册');

    const rawToken = await generateResetToken(email);
    await sendResetEmail(email, rawToken);
    res.send('重置邮件已发送,请查收邮箱');
  } catch (err) {
    console.error(err);
    res.status(500).send('服务器内部错误');
  }
});

四、接收令牌、验证并传递用户数据到前端

这里需要用模板引擎(比如EJS)渲染动态页面,先配置Express模板引擎:

const express = require('express');
const app = express();

// 配置EJS模板引擎
app.set('view engine', 'ejs');
app.set('views', './views'); // 你的模板文件存放在views文件夹

1. 验证令牌并渲染重置表单

app.get('/reset-password', async (req, res) => {
  const { token } = req.query;
  if (!token) return res.status(400).send('缺少重置令牌');

  try {
    // 查找所有未过期的令牌记录
    const [users] = await db.query(
      'SELECT * FROM users WHERE reset_expiry > NOW()',
    );

    // 遍历用户对比令牌哈希(bcrypt无法反向查询,只能逐个验证)
    let validUser = null;
    for (const user of users) {
      const isValid = await bcrypt.compare(token, user.reset_token);
      if (isValid) {
        validUser = user;
        break;
      }
    }

    if (!validUser) return res.status(400).send('令牌无效或已过期');

    // 将用户邮箱(非敏感信息)和令牌传递到模板
    res.render('reset-password', { email: validUser.email, token });
  } catch (err) {
    console.error(err);
    res.status(500).send('服务器内部错误');
  }
});

2. 前端重置表单模板(views/reset-password.ejs)

<!DOCTYPE html>
<html>
<head>
  <title>重置密码</title>
</head>
<body>
  <h1>重置你的密码</h1>
  <form action="/reset-password" method="POST">
    <!-- 隐藏字段,传递令牌和用户邮箱(后端会再次验证) -->
    <input type="hidden" name="token" value="<%= token %>">
    <input type="hidden" name="email" value="<%= email %>">
    
    <label>新密码:</label>
    <input type="password" name="newPassword" required>
    
    <button type="submit">确认重置</button>
  </form>
</body>
</html>

五、处理密码重置提交

app.post('/reset-password', async (req, res) => {
  const { token, email, newPassword } = req.body;
  if (!token || !email || !newPassword) return res.status(400).send('参数不全');

  try {
    // 再次验证令牌和用户有效性
    const [users] = await db.query(
      'SELECT * FROM users WHERE email = ? AND reset_expiry > NOW()',
      [email]
    );
    if (!users.length) return res.status(400).send('用户不存在或令牌过期');

    const user = users[0];
    const isValidToken = await bcrypt.compare(token, user.reset_token);
    if (!isValidToken) return res.status(400).send('令牌无效');

    // 哈希新密码并更新用户表,同时清空令牌记录
    const hashedPassword = await bcrypt.hash(newPassword, 10);
    await db.query(
      'UPDATE users SET password = ?, reset_token = NULL, reset_expiry = NULL WHERE email = ?',
      [hashedPassword, email]
    );

    res.send('密码重置成功!现在可以登录了');
  } catch (err) {
    console.error(err);
    res.status(500).send('服务器内部错误');
  }
});

六、关键安全注意事项

  • 令牌过期机制:必须设置过期时间(比如1小时),防止令牌被长期滥用
  • HTTPS传输:生产环境一定要用HTTPS,避免令牌在传输过程中被截获
  • 令牌一次性:重置密码后立即清空数据库里的令牌记录,防止重复使用
  • 避免敏感信息:URL里只放令牌,不要明文传递用户ID等敏感数据

内容的提问来源于stack exchange,提问作者Miellies

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 11:06:00