首页 / Node.js 教程 / crypto 加密

Node.js 教程

crypto 加密

本教程共 76 篇 · 第 30 篇 · 更新于 2026-07-25 · 约 6 分钟阅读

Node.jscrypto加密哈希安全

30. crypto 加密

本节目标:哈希、HMAC、对称与非对称加密和随机数的实用场景。

crypto 是 Node.js 里跟安全打交道最多的模块。哈希、签名、加解密、随机数——这些操作看似高深,实际用上几次就会发现 API 设计得挺直白。这一章我会挑最常用的几个场景,给你能直接跑的代码,顺便说说哪些坑不能踩。

哈希:给数据算指纹

哈希函数能把任意长度的输入转成固定长度的字符串。同一个输入永远得到同一个输出,但反向推导几乎不可能。最常见的用途是校验文件完整性或存储密码的「加盐哈希」。

Node.js 里算哈希是个链式调用:

import crypto from 'node:crypto';

const hash = crypto.createHash('sha256')
  .update('Hello, world!')
  .digest('hex');

console.log(hash);

createHash 接收算法名,update 可以调用多次来追加数据,digest('hex') 输出十六进制字符串。如果需要二进制,可以传 'base64''binary'

Warning

MD5 和 SHA-1 已经被证明存在碰撞风险,别再用于安全场景。新项目直接用 SHA-256 或更长的 SHA-512。

校验文件完整性

下载大文件时,服务端通常会提供一个校验值。你可以在本地重新计算,比对是否一致:

import crypto from 'node:crypto';
import { createReadStream } from 'node:fs';

function hashFile(filepath, algorithm = 'sha256') {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash(algorithm);
    const stream = createReadStream(filepath);

    stream.on('error', reject);
    stream.on('data', chunk => hash.update(chunk));
    stream.on('end', () => resolve(hash.digest('hex')));
  });
}

const localHash = await hashFile('./downloaded.zip');
console.log('File hash:', localHash);
// 跟服务端提供的 hash 对比即可

HMAC:带密钥的哈希

HMAC(Hash-based Message Authentication Code)在哈希基础上加了一个密钥。这样就算攻击者知道你的算法,没有密钥也伪造不出正确的摘要。API 签名、Webhook 校验都靠它。

import crypto from 'node:crypto';

const secret = 'my-secret-key';
const message = 'amount=100&orderId=12345';

const hmac = crypto.createHmac('sha256', secret)
  .update(message)
  .digest('hex');

console.log('Signature:', hmac);

服务端收到请求后,用同样的密钥和算法重新算一遍签名,跟客户端传过来的比对。这里有个细节:比对签名时不要用普通的 ===,要用 crypto.timingSafeEqual,防止时序攻击猜出正确签名。

const expected = Buffer.from(hmac, 'hex');
const actual = Buffer.from(clientSignature, 'hex');

if (expected.length !== actual.length) {
  throw new Error('Invalid signature');
}

if (!crypto.timingSafeEqual(expected, actual)) {
  throw new Error('Invalid signature');
}

对称加密:AES

哈希是单向的,不能还原。如果想加密一段数据,之后还能解密回来,就得用对称加密。AES-256-GCM 是目前推荐的方案,它同时提供加密和认证,能检测出数据是否被篡改。

import crypto from 'node:crypto';

const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;
const IV_LENGTH = 16;
const AUTH_TAG_LENGTH = 16;

function encrypt(plainText, key) {
  const iv = crypto.randomBytes(IV_LENGTH);
  const cipher = crypto.createCipheriv(ALGORITHM, key, iv);

  const encrypted = Buffer.concat([
    cipher.update(plainText, 'utf8'),
    cipher.final(),
  ]);

  const authTag = cipher.getAuthTag();

  // 把 iv + authTag + ciphertext 拼在一起
  return Buffer.concat([iv, authTag, encrypted]).toString('base64');
}

function decrypt(cipherText, key) {
  const data = Buffer.from(cipherText, 'base64');

  const iv = data.subarray(0, IV_LENGTH);
  const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH);
  const encrypted = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH);

  const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
  decipher.setAuthTag(authTag);

  const decrypted = Buffer.concat([
    decipher.update(encrypted),
    decipher.final(),
  ]);

  return decrypted.toString('utf8');
}

// 生成 32 字节的密钥(实际项目里从环境变量读取)
const key = crypto.randomBytes(KEY_LENGTH);

const original = 'Sensitive user data';
const sealed = encrypt(original, key);
console.log('Encrypted:', sealed);

const opened = decrypt(sealed, key);
console.log('Decrypted:', opened);
Note

我用的是 aes-256-gcm,不是老教程里常见的 aes-256-cbc。GCM 模式自带认证标签(auth tag),能防止密文被篡改;CBC 没有这个功能,需要额外配 HMAC,容易配错。新项目首选 GCM。

每次加密都要生成新的 IV(初始化向量),并且把 IV 跟密文一起存储或传输。密钥则必须严格保密,丢了密钥就意味着数据永久无法解密。

密码存储:用 scrypt

用户密码绝对不能明文存,也不要直接用 SHA-256 哈希——因为同样的密码哈希值一样,攻击者可以用彩虹表批量破解。正确做法是给每个密码配一个随机「盐」(salt),再用专门设计来「慢」的算法去算。

crypto.scryptSync 就是干这个的:

import crypto from 'node:crypto';

function hashPassword(password) {
  const salt = crypto.randomBytes(16).toString('hex');
  const hash = crypto.scryptSync(password, salt, 64).toString('hex');
  return { salt, hash };
}

function verifyPassword(password, salt, hash) {
  const computed = crypto.scryptSync(password, salt, 64).toString('hex');
  return computed === hash;
}

// 注册时
const { salt, hash } = hashPassword('mySecret123');
// 把 salt 和 hash 都存进数据库

// 登录时
const valid = verifyPassword('mySecret123', salt, hash);
console.log('Password valid:', valid);

scrypt 的第三个参数是输出长度(字节)。它内部会反复迭代,故意让计算变慢,这样就算数据库泄露,攻击者暴力破解的成本也会高很多。

Tip

生产环境里,bcrypt 和 argon2 也是好选择。Node.js 内置了 crypto.scrypt,如果你不想多装依赖,用它完全够打。

安全随机数

生成 Token、Session ID、盐值时,不要用 Math.random(),它的随机性不够强。crypto.randomBytes 用的是操作系统的熵池,适合安全场景。

import crypto from 'node:crypto';

// 生成 32 字节的随机值,转 hex
const token = crypto.randomBytes(32).toString('hex');
console.log('Token:', token);

// 生成 URL 安全的随机字符串
const safeToken = crypto.randomBytes(24).toString('base64url');
console.log('Base64url:', safeToken);

base64url 编码把 +/ 换成了 -_,并且去掉末尾的 =,可以直接放进 URL 或 Cookie 里。

查看本机支持的算法

想知道当前 OpenSSL 版本支持哪些算法,可以打印列表:

console.log('Hashes:', crypto.getHashes());
console.log('Ciphers:', crypto.getCiphers());

不同 Node.js 版本和不同操作系统,这个列表可能略有差异。如果你写的代码要跨平台跑,先确认目标算法在两边都可用。