一次由“双重加密”引发的 Node.js 登录失败血案

一次由"双重加密"引发的 Node.js 登录失败血案

1. 问题描述

在一个基于 Node.js, Express 和 MongoDB 的用户认证系统中,我遇到了一个棘手的登录问题。尽管我确信在 Postman 中使用了正确的用户名(admin)和密码(admin123),但登录接口 (/api/auth/login) 始终返回 400 Bad Request,并附带错误信息:

json 复制代码
{

    "code": 400,

    "message": "用户名或密码错误"

}

截图如下:

在排除了服务器未启动、端口错误、路由404等基本配置问题后,我确信问题出在登录逻辑的内部,而不是应用的基础架构上。

2. 问题分析与原因

为了弄清楚密码验证环节到底发生了什么,我采取了最直接也最有效的调试手段:在核心代码中添加日志。

我定位到了 models/User.js 文件中用于比较密码的实例方法 comparePassword,并在其中加入了详细的 console.log,以便观察输入密码和数据库中存储的哈希值。

javascript 复制代码
// models/User.js

userSchema.methods.comparePassword = async function(candidatePassword) {
  try {
    console.log('--- 开始密码验证 ---');
    console.log('输入的密码 (Candidate Password):', candidatePassword);
    console.log('数据库中的哈希密码 (Stored Hash):', this.password);
    const isMatch = await bcrypt.compare(candidatePassword, this.password);
    console.log('密码是否匹配 (isMatch):', isMatch);
    console.log('--- 密码验证结束 ---');
    return isMatch;
  } catch (error) {
    // ...
  }
};

重启服务并再次尝试登录后,我在终端看到了决定性的日志输出:

bash 复制代码
--- 开始密码验证 ---
输入的密码 (Candidate Password): admin123
数据库中的哈希密码 (Stored Hash): $2a$10$xtYl0jQPCJukil5IEWS6reAJEBfQL9l.52TPhoj1BBCBmj9GFSQyi
密码是否匹配 (isMatch): false
--- 密码验证结束 ---

日志清晰地表明 isMatch 的结果是 false ,密码比对失败!

这立刻让我警觉起来:为什么用同一个原始密码,bcrypt 却认为它们不匹配?答案只有一个:数据库中存储的密码哈希是错误的。通过追溯代码,我发现了问题的根源------密码被加密了两次。

  • 第一次加密: 在数据初始化脚本 scripts/initData.js 中,我为了创建管理员账号,手动对原始密码进行了哈希处理。
javascript 复制代码
    // scripts/initData.js
    const hashedPassword = await bcrypt.hash('admin123', 10);
    await User.create({
        username: 'admin',
        password: hashedPassword, // <- 这里传入了已加密的密码
        // ...
    });
  • 第二次加密: 在用户模型 models/User.js 中,存在一个 Mongoose 的 pre('save') 中间件。这个中间件的职责是在任何 User 文档保存到数据库之前,自动检查 password 字段是否被修改,如果被修改,就对其进行哈希处理。
js 复制代码
    // models/User.js
    userSchema.pre('save', async function(next) {
      if (!this.isModified('password')) return next();
      
      this.password = await bcrypt.hash(this.password, salt); // <- 自动加密
      next();
    });

当 initData.js 脚本调用 User.create() 时,整个流程变成了:

  1. 脚本将 'admin123' 加密成 哈希A。
  2. User.create() 接收到 哈希A 作为密码
  3. 在数据即将存入数据库前,pre('save') 钩子被触发。
  4. 钩子检测到 password 字段(也就是哈希A)是"新"的,于是它把这个哈希A本身又当成一个普通字符串,进行了第二次哈希,生成了哈希B。
  5. 最终,存入数据库的是这个被错误加密了两次的哈希B

这就是导致登录时密码比对永远失败的根本原因。

3. 解决思路

问题的症结在于加密操作的职责不清。Mongoosepre('save') 钩子是处理此类逻辑的理想场所,它能确保无论是通过哪个业务流程创建或更新用户,密码加密的逻辑都是统一且唯一的。

因此,解决思路非常明确:
移除初始化脚本中的手动加密步骤,将密码加密的职责完全交给 User 模型自己。

4. 解决方案

我修改了 scripts/initData.js 文件,不再对密码进行预处理,而是直接将明文密码传递给 User.create() 方法。

js 复制代码
// scripts/initData.js 

const adminPassword = 'admin123';
// 不再需要手动加密,交给 User 模型的 pre('save') 钩子处理
// const hashedPassword = await bcrypt.hash(adminPassword, 10);

await User.create({
    username: 'admin',
    email: 'admin@example.com',
    // 直接提供原始密码
    password: adminPassword,
    roles: [adminRole._id],
    status: 1
});

在代码修正后,执行了最后也是最关键的一步:

  1. 重新运行数据初始化脚本:npm run init-data,用正确加密的密码覆盖数据库中的旧数据。
  1. 重启服务:npm start。
  1. 再次登录,成功获取到了 token,问题圆满解决。
json 复制代码
{
    "code": 200,
    "message": "登录成功",
    "data": {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjY4NTY1MTE0MDk1YjM1YWExMGUwNjkwNyIsInVzZXJuYW1lIjoiYWRtaW4iLCJpYXQiOjE3NTA0ODczMjYsImV4cCI6MTc1MTA5MjEyNn0.avkF5iOAgp4o45dFHSijtu4tgy8J4snO3TwnzXUAbr4"
    }
}

这次排错经历是一个经典的教训:在多层抽象(模型、服务、脚本)中,确保单一职责原则,避免功能重叠,是防止此类诡异 Bug 的关键。

相关推荐
Jing_Rainbow15 小时前
【Vue-2/Lesson62(2025-12-10)】模块化与 Node.js HTTP 服务器开发详解🧩
前端·vue.js·node.js
TE-茶叶蛋17 小时前
NestJS中使用TypeORM
node.js
Drift_Dream18 小时前
Node.js 第3课:Express.js框架入门
node.js
c***69301 天前
node.js下载、安装、设置国内镜像源(永久)(Windows11)
node.js
全栈前端老曹1 天前
【包管理】npm init 项目名后底层发生了什么的完整逻辑
前端·javascript·npm·node.js·json·包管理·底层原理
callJJ1 天前
MCP配置与实战:深入理解现代开发工具链
javascript·node.js·vue·mcp·windsurf
程序员爱钓鱼1 天前
Node.js 编程实战:测试与调试 —— 日志与监控方案
前端·后端·node.js
雪域迷影1 天前
Node.js中使用node-redis库连接redis服务端并存储数据
数据库·redis·node.js
winfredzhang1 天前
从零构建:基于 Node.js 的全栈视频资料管理系统开发实录
css·node.js·html·音视频·js·收藏,搜索,缩略图
遗憾随她而去.1 天前
Webpack 面试题
前端·webpack·node.js