crypto 加密
本教程共 76 篇 · 第 30 篇 · 更新于 2026-07-25 · 约 6 分钟阅读
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'。
WarningMD5 和 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 版本和不同操作系统,这个列表可能略有差异。如果你写的代码要跨平台跑,先确认目标算法在两边都可用。