基于Node.js+MySQL的密码找回开发求助:bcrypt令牌链路实现
密码找回模块完整实现指南
我来一步步帮你搞定这个密码找回流程,结合你用到的Node.js、MySQL和那些npm包,分模块拆解核心步骤:
一、先理清核心逻辑(关键误区提醒)
首先要明确:bcrypt是用来哈希敏感数据(比如密码、令牌的哈希值)的,不能直接把bcrypt哈希后的字符串当URL令牌用(太长且没必要)。正确的流程是:
- 用户提交找回请求→生成随机原始令牌
- 把令牌的bcrypt哈希值+过期时间存到MySQL用户表
- 将原始令牌拼到重置链接里,用nodemailer发给用户
- 用户点击链接→后端验证令牌有效性(对比哈希+检查过期)
- 验证通过后,把用户非敏感数据传递到重置表单页面
- 用户提交新密码→再次验证令牌后更新密码,清空令牌记录
二、前置准备: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
相关产品推荐
相关产品推荐

