node-argon2 教程

node-argon2 是 Argon2 参考实现的 Node.js 绑定,提供适合密码哈希的 API 和 PHC 字符串格式。

1. 安装与导入

npm install argon2

它自带 TypeScript 类型声明,不需要安装 @types/argon2

结合当前 CommonJS 编译配置,使用官方 TypeScript 示例风格:

import * as argon2 from "argon2";

argon2 包含原生二进制组件。当前官方 README 表示最新版本只针对 Node.js 22 及以上进行测试,并为常见平台提供预编译二进制。安装失败时应先检查 Node.js 版本、操作系统支持和官方安装说明,而不是随意降低安全参数。

2. argon2.hash():哈希密码

const passwordHash = await argon2.hash("correct horse battery staple", {
  type: argon2.argon2id,
});

推荐封装:

async function hashPassword(password: string): Promise<string> {
  return argon2.hash(password, {
    type: argon2.argon2id,
  });
}

注册时只保存 passwordHash

const user = {
  id: "user-123",
  username: "zhangsan",
  passwordHash: await hashPassword(inputPassword),
};

不要自己使用固定盐:

// 不推荐:破坏默认的安全随机盐策略
await argon2.hash(password, {
  salt: Buffer.from("fixed-salt"),
});

默认情况下库会生成随机盐并将其包含在结果中。

3. 是否需要修改默认参数

官方 README 的建议是:用于密码哈希时,通常无需修改默认安全参数。

学习时优先使用:

const passwordHash = await argon2.hash(password, {
  type: argon2.argon2id,
});

只有在你已经完成服务器压测、明确安全目标和资源预算后,再统一定义参数:

const passwordHashOptions: argon2.Options & { raw?: false } = {
  type: argon2.argon2id,
  memoryCost: 65536,
  timeCost: 3,
  parallelism: 4,
};

这里的数字只是展示参数含义,不应脱离当前库版本、服务器硬件和安全标准盲目复制。

4. argon2.verify():验证密码

参数顺序是:

argon2.verify(数据库中的哈希, 用户输入的密码)

完整示例:

async function verifyPassword(
  passwordHash: string,
  inputPassword: string
): Promise<boolean> {
  return argon2.verify(passwordHash, inputPassword);
}

业务代码应区分“不匹配”和“系统异常”:

try {
  const matched = await argon2.verify(
    user.passwordHash,
    inputPassword
  );

  if (!matched) {
    // 正常的登录失败
  }
} catch (error) {
  // 内部错误:记录安全日志并返回通用服务错误
}

5. argon2.needsRehash():检查是否应升级

needsRehash() 比较哈希中记录的参数与当前要求:

if (argon2.needsRehash(user.passwordHash)) {
  user.passwordHash = await argon2.hash(inputPassword, {
    type: argon2.argon2id,
  });
}

应当只在密码验证成功后执行:

async function verifyAndUpgradePassword(
  user: { passwordHash: string },
  inputPassword: string
): Promise<boolean> {
  const matched = await argon2.verify(
    user.passwordHash,
    inputPassword
  );

  if (!matched) {
    return false;
  }

  if (argon2.needsRehash(user.passwordHash)) {
    user.passwordHash = await argon2.hash(inputPassword, {
      type: argon2.argon2id,
    });
  }

  return true;
}

如果团队使用显式参数策略,检查和重新哈希必须传入同一策略:

if (argon2.needsRehash(user.passwordHash, passwordHashOptions)) {
  user.passwordHash = await argon2.hash(
    inputPassword,
    passwordHashOptions
  );
}

6. argon2.argon2id

argon2.argon2id 是库导出的类型常量:

const passwordHash = await argon2.hash(password, {
  type: argon2.argon2id,
});

当前库默认也是 Argon2id。显式写出有三个学习价值:

7. 一个注册与登录核心函数

import * as argon2 from "argon2";

interface User {
  id: string;
  username: string;
  passwordHash: string;
}

async function createUser(
  username: string,
  password: string
): Promise<User> {
  const passwordHash = await argon2.hash(password, {
    type: argon2.argon2id,
  });

  return {
    id: crypto.randomUUID(),
    username,
    passwordHash,
  };
}

async function checkLogin(
  user: User,
  inputPassword: string
): Promise<boolean> {
  const matched = await argon2.verify(
    user.passwordHash,
    inputPassword
  );

  if (!matched) {
    return false;
  }

  if (argon2.needsRehash(user.passwordHash)) {
    user.passwordHash = await argon2.hash(inputPassword, {
      type: argon2.argon2id,
    });
  }

  return true;
}

注意:示例中的 crypto.randomUUID() 假定你导入了 randomUUID 或使用了可用的全局 Web Crypto。为了与 Node.js 代码风格保持一致,实际案例会显式写 import { randomUUID } from "node:crypto"

8. 常见错误

// 错误:保存明文
user.password = inputPassword;

// 错误:重新 hash 后比较字符串
await argon2.hash(inputPassword) === user.passwordHash;

// 错误:把参数顺序颠倒
await argon2.verify(inputPassword, user.passwordHash);

// 错误:把 passwordHash 返回给客户端
res.json(user);

正确做法是建立安全响应类型:

interface PublicUser {
  id: string;
  username: string;
}

const publicUser: PublicUser = {
  id: user.id,
  username: user.username,
};