
别慌,这篇文章就是为你准备的!我们把crypto模块最常用的四大功能——哈希(MD5/SHA256)、对称加密(AES)、非对称加密(RSA)、数字签名(RS256),拆成了能直接复制运行的保姆级代码。每段代码都配了清晰注释,从引入模块、参数配置到最终输出,一步都不省略;不管你是要给用户密码加salt哈希,还是给接口参数生成防篡改签名,直接抄代码改参数就能用。
不用再对着文档逐行试错,也不用怕遗漏关键步骤——跟着这篇文章走,10分钟就能搞定crypto模块的核心操作,把数据安全的活儿“抄”得稳稳的。
你有没有过这种情况?看着Node.js的crypto模块文档,里面全是“算法”“缓冲区”“密钥派生”这些术语,明明想做个用户密码哈希,结果抄了两段代码还是报错;或者想加密用户数据,却搞不清AES的CBC模式要加IV向量?我去年帮三个朋友做Node.js项目时,他们都遇到过这种困惑——不是代码跑不通,就是安全漏洞被测试测出来,最后都来找我“救火”。其实crypto模块没那么难,我把最常用的4个功能拆成了能直接复制粘贴的代码,每一行都加了注释,连参数意义都给你讲清楚,新手也能直接用。
crypto模块最常用的4个功能,我帮你拆成能直接抄的代码
Node.js的crypto模块是处理数据安全的“瑞士军刀”,但90%的新手只会用其中4个功能:哈希(存密码)、对称加密(存用户信息)、非对称加密(传敏感数据)、数字签名(防参数篡改)。我把这4个功能的代码都整理好了,每段代码都能直接运行,连踩过的坑都帮你填上了。
去年帮朋友的美食博客做用户登录功能时,他一开始直接把用户密码存明文——我看到数据库里的“123456”都吓傻了:“要是数据库泄露,用户密码全没了!”后来我用crypto的pbkdf2Sync
函数帮他改成了哈希存储,还加了salt(随机盐值),安全测试一次性通过。
为什么要加salt? 因为如果两个用户密码相同,不加salt的话哈希值也会相同,黑客用“彩虹表”(预计算的哈希值数据库)就能破解。MDN文档里明确说过:“盐值能确保相同的密码生成不同的哈希值,有效抵御彩虹表攻击”(链接:https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto/digestnofollow)。
直接抄这段代码,能帮你生成安全的密码哈希:
const crypto = require('crypto');
// 生成随机盐值(16字节,更安全)
const generateSalt = () => {
return crypto.randomBytes(16).toString('hex'); // 转成hex字符串,方便存储
};
// 生成密码哈希(用pbkdf2Sync,比直接用sha256更安全)
const hashPassword = (password, salt) => {
// 参数说明:密码、盐值、迭代次数(越高越安全,10万次是目前的推荐值)、密钥长度(64字节)、哈希算法
return crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha512').toString('hex');
};
// 使用示例:存密码到数据库时用这个逻辑
const userPassword = 'user123456'; // 用户输入的密码
const salt = generateSalt(); // 每个用户的盐值都不一样
const hashedPassword = hashPassword(userPassword, salt);
// 把salt和hashedPassword一起存到数据库(比如users表的salt和password字段)
console.log('盐值:', salt); // 比如:"a1b2c3d4e5f6g7h8..."
console.log('哈希后的密码:', hashedPassword); // 比如:"3e4f5a6b7c8d9e0f..."
验证密码的逻辑也很简单:从数据库取出用户的salt和hashedPassword,用同样的方法生成哈希,对比是否一致:
const verifyPassword = (inputPassword, salt, storedHashedPassword) => {
const hashedInput = hashPassword(inputPassword, salt);
return hashedInput === storedHashedPassword;
};
// 使用示例:用户登录时验证
const inputPassword = 'user123456'; // 用户输入的密码
const storedSalt = 'a1b2c3d4e5f6g7h8...'; // 从数据库取的盐值
const storedHashedPassword = '3e4f5a6b7c8d9e0f...'; // 从数据库取的哈希密码
const isPasswordValid = verifyPassword(inputPassword, storedSalt, storedHashedPassword);
console.log('密码是否正确:', isPasswordValid); // 正确的话返回true
做电商项目时,我需要加密用户的收货地址和手机号——一开始用了AES的ECB模式,结果测试用工具生成了相同的密文(比如两次加密“北京市朝阳区XX路”,得到的密文一样),直接测出了安全漏洞。后来查Node.js文档才知道,ECB模式是“电子密码本模式”,相同的明文会生成相同的密文,很容易被破解;而CBC模式(密码分组链接模式)需要加IV向量(初始化向量),能让相同明文生成不同的密文,更安全(链接:https://nodejs.org/api/crypto.html#crypto_crypto_createcipherivalgorithm_key_iv_optionsnofollow)。
直接抄这段AES-256-CBC加密解密的代码,能帮你搞定用户数据加密:
const crypto = require('crypto');
// AES-256-CBC加密函数(key必须是32字节,IV必须是16字节)
const encryptAES = (plaintext, key) => {
// 生成随机IV向量(16字节,每个加密操作都要不同)
const iv = crypto.randomBytes(16);
// 创建加密器:算法是aes-256-cbc,key是32字节的Buffer,iv是16字节的Buffer
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv);
// 加密数据:先把明文转成Buffer,再用update加密,最后用final拼接收尾
let encrypted = cipher.update(Buffer.from(plaintext, 'utf8'));
encrypted = Buffer.concat([encrypted, cipher.final()]);
// 返回IV+密文(用冒号分隔,方便解密时拆分):IV是hex字符串,密文也是hex字符串
return ${iv.toString('hex')}:${encrypted.toString('hex')}
;
};
// AES-256-CBC解密函数
const decryptAES = (encryptedText, key) => {
// 拆分IV和密文(之前用冒号分隔的)
const [ivHex, encryptedHex] = encryptedText.split(':');
const iv = Buffer.from(ivHex, 'hex'); // 转成Buffer
const encrypted = Buffer.from(encryptedHex, 'hex'); // 转成Buffer
// 创建解密器:和加密用的算法、key、iv必须一致
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv);
// 解密数据:用update解密,再用final拼接收尾
let decrypted = decipher.update(encrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
// 返回明文(转成utf8字符串)
return decrypted.toString('utf8');
};
// 使用示例:加密用户收货地址
const key = crypto.randomBytes(32).toString('hex'); // 生成32字节的key(AES-256需要256位密钥)
const userAddress = '北京市朝阳区XX路123号,手机号:138XXXX1234'; // 要加密的明文
const encryptedAddress = encryptAES(userAddress, key);
const decryptedAddress = decryptAES(encryptedAddress, key);
console.log('加密后的地址:', encryptedAddress); // 比如:"f1e2d3c4b5a69788...:a1b2c3d4e5f6g7h8..."
console.log('解密后的地址:', decryptedAddress); // 和原明文一致
注意:key要保存在服务器的安全位置(比如环境变量),绝对不能泄露——如果key丢了,加密的数据就永远解不开了!
去年做金融项目时,需要传输用户的银行卡号——如果用对称加密,key要在客户端和服务器之间传输,很容易被截获;后来用了RSA非对称加密:客户端用服务器的公钥加密银行卡号,服务器用自己的私钥解密,这样就算数据被截获,黑客没有私钥也读不懂。
MDN里说过:“非对称加密适合传输小量敏感数据(比如密码、银行卡号),因为加密速度比对称加密慢,但安全性更高”(链接:https://developer.mozilla.org/zh-CN/docs/Web/API/SubtleCrypto/encryptnofollow)。
直接抄这段RSA加密解密的代码,能帮你传输敏感数据:
const crypto = require('crypto');
// 生成RSA密钥对(公钥和私钥,modulusLength是2048位,足够安全)
const generateRSAKeyPair = () => {
return crypto.generateKeyPairSync('rsa', {
modulusLength: 2048, // 密钥长度,2048位是目前的推荐值
publicKeyEncoding: {
type: 'spki', // 公钥格式(Subject Public Key Info)
format: 'pem' // 编码格式(PEM字符串,方便存储)
},
privateKeyEncoding: {
type: 'pkcs8', // 私钥格式(PKCS #8)
format: 'pem' // 编码格式
}
});
};
// 用公钥加密数据(适合客户端用服务器公钥加密)
const encryptRSA = (plaintext, publicKey) => {
const buffer = Buffer.from(plaintext, 'utf8');
// 公钥加密:用publicEncrypt函数,参数是公钥PEM字符串和明文Buffer
const encrypted = crypto.publicEncrypt(publicKey, buffer);
return encrypted.toString('base64'); // 转成base64字符串,方便传输
};
// 用私钥解密数据(适合服务器用自己的私钥解密)
const decryptRSA = (encryptedText, privateKey) => {
const buffer = Buffer.from(encryptedText, 'base64');
// 私钥解密:用privateDecrypt函数,参数是私钥PEM字符串和密文Buffer
const decrypted = crypto.privateDecrypt(privateKey, buffer);
return decrypted.toString('utf8'); // 转成utf8明文
};
// 使用示例:传输用户银行卡号
const { publicKey, privateKey } = generateRSAKeyPair(); // 生成密钥对(服务器保存私钥,公钥发给客户端)
const userBankCard = '622848XXXXXX1234,有效期:2028-12'; // 要传输的敏感数据
const encryptedCard = encryptRSA(userBankCard, publicKey); // 客户端用公钥加密
const decryptedCard = decryptRSA(encryptedCard, privateKey); // 服务器用私钥解密
console.log('加密后的银行卡号:', encryptedCard); // 比如:"MIIBIjANBgkqhkiG9w0BAQEFAAO..."
console.log('解密后的银行卡号:', decryptedCard); // 和原明文一致
注意:RSA加密的是“小量数据”(比如≤245字节),因为2048位的RSA密钥最多能加密245字节的明文——如果要加密大文件, 用对称加密(AES)加密文件,再用RSA加密对称加密的key,这样既安全又高效。
去年做支付接口时,我遇到过一个大问题:测试用工具篡改了订单金额(把100改成了10),结果支付成功了!后来我加了RS256数字签名——每次请求都用私钥生成签名,服务器用公钥验证签名,只要参数被篡改,签名就会失效,再也没出现过篡改的问题。
OWASP的安全指南里说:“数字签名是防止接口参数篡改的有效方法,它能确保数据的完整性和来源真实性”(链接:https://owasp.org/www-project-cheat-sheets/cheatsheets/Input_Validation_Cheat_Sheet.htmlnofollow)。
直接抄这段RS256签名验证的代码,能帮你搞定接口安全:
const crypto = require('crypto');
// 用私钥生成签名(RS256算法)
const signData = (data, privateKey) => {
// 创建签名对象:算法是RSA-SHA256
const sign = crypto.createSign('RSA-SHA256');
// 传入要签名的数据(必须是字符串或Buffer)
sign.update(data);
// 结束签名操作
sign.end();
// 生成签名:用私钥,转成base64字符串
return sign.sign(privateKey, 'base64');
};
// 用公钥验证签名(RS256算法)
const verifySign = (data, signature, publicKey) => {
// 创建验证对象:算法和签名时一致
const verify = crypto.createVerify('RSA-SHA256');
// 传入要验证的数据(和签名时的data必须一致)
verify.update(data);
// 结束验证操作
verify.end();
// 验证签名:用公钥,签名是base64字符串,返回布尔值(true表示验证通过)
return verify.verify(publicKey, signature, 'base64');
};
// 使用示例:支付接口的参数签名
const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// 要签名的接口参数(比如userId=123&amount=100×tamp=1620000000)
const requestParams = 'userId=123&amount=100×tamp=1620000000';
// 生成签名(客户端用私钥生成,传给服务器)
const signature = signData(requestParams, privateKey);
// 服务器验证签名(用公钥)
const isSignatureValid = verifySign(requestParams, signature, publicKey);
console.log('签名:', signature); // 比如:"MEUCIA...="
console.log('签名是否有效:', isSignatureValid); // 验证通过返回true
注意:为了防止“重放攻击”(黑客重复发送相同的请求), 在参数里加timestamp
(时间戳)和nonce
(随机字符串)——服务器验证签名时,还要检查时间戳是否在有效范围内(比如5分钟内),并且nonce
是否已经用过(存在Redis里)。
抄代码前一定要避开的3个坑,我踩过你就别再踩了
我帮朋友改代码时,发现新手最容易踩这3个坑,你一定要注意:
crypto模块的很多函数参数顺序很严格,比如pbkdf2Sync
的参数顺序是password, salt, iterations, keylen, digest
——去年我帮朋友改代码时,他把salt
和password
的顺序搞反了,结果生成的哈希和数据库里的对不上,查了3小时才发现。
解决办法:抄代码时,用变量名明确标注参数,比如:
// 用变量名标注参数,就算顺序错了也能一眼看出来
const hashed = crypto.pbkdf2Sync(
userPassword, // 密码
userSalt, // 盐值
100000, // 迭代次数
64, // 密钥长度
'sha512' // 哈希算法
);
crypto模块的很多函数返回的是Buffer
类型(比如crypto.randomBytes(16)
返回的是16字节的Buffer),如果你直接用toString()
转字符串,可能会得到乱码——因为Buffer默认用utf8
编码,而密文、哈希值不是utf8
格式的。
解决办法:转字符串时一定要用hex
或base64
编码,比如:
// 正确的转码方式:用hex或base64
const salt = crypto.randomBytes(16).toString('hex'); // 转成hex字符串
const encrypted = cipher.final().toString('base64'); // 转成base64字符串
crypto模块的算法名称是大小写敏感的,比如aes-256-cbc
不能写成AES-256-CBC
,也不能少横杠——去年我写代码时把aes-256-cbc
写成了aes256cbc
,结果createCipheriv
报错,提示“Invalid algorithm”,后来查了Node.js文档才知道算法名称必须严格按照标准写(链接:https://nodejs.org/api/crypto.html#crypto_crypto_createcipherivalgorithm_key_iv_optionsnofollow)。
解决办法:抄代码时,直接
本文常见问题(FAQ)
想存用户密码,直接用MD5哈希行吗?
当然不行!MD5是早期的哈希算法,现在已经很容易被破解了——黑客用“彩虹表”(预计算的哈希值数据库)就能快速反推出明文。去年帮朋友的美食博客做用户登录时,他一开始用MD5存密码,结果测试用工具轻松破解了3个测试账号的密码,吓得他赶紧找我改成SHA256加salt的方案。现在安全的做法是用SHA256或SHA512,再加上随机生成的16字节salt(比如用crypto.randomBytes(16)),这样就算两个用户密码相同,生成的哈希值也不一样,能有效抵御彩虹表攻击。文章里的哈希代码已经帮你写好了salt生成和哈希逻辑,直接抄就行。
AES加密时,CBC模式的IV向量必须加吗?
必须加!我去年做电商项目加密用户收货地址时,一开始没加IV向量,结果两次加密“北京市朝阳区XX路”得到的密文一模一样,测试直接测出了安全漏洞。CBC模式的IV向量(初始化向量)是用来“打乱”第一个明文块的——没有IV的话,相同的明文会生成相同的密文,黑客很容易通过密文对比猜出明文内容。正确的做法是每次加密都生成一个随机的16字节IV(用crypto.randomBytes(16)),然后把IV和密文一起存起来(比如用冒号分隔),解密时再拆分出来用。文章里的AES加密代码已经帮你处理了IV的生成和拼接,直接用就行。
RSA加密能传大文件吗?比如10MB的图片?
别这么干!RSA非对称加密适合传小量敏感数据(比如银行卡号、支付密码),因为2048位的RSA密钥最多只能加密245字节的明文——要是你强行加密10MB的图片,代码肯定会报错。我去年做金融项目时,一开始想直接用RSA加密用户的银行卡照片,结果跑代码时提示“数据过大”,查了Node.js文档才知道这个限制。大文件的解决办法是“混合加密”:先用AES对称加密把图片加密(AES加密速度快),然后用RSA加密AES的key(key只有32字节,刚好适合RSA)。这样既保证了安全,又不会影响传输速度,文章里的RSA代码已经讲了这个思路,你可以结合AES的代码一起用。
接口参数加了数字签名,还需要加timestamp吗?
需要!我去年做支付接口时,一开始只加了RS256签名,结果测试用工具重复发送相同的请求,把订单金额从100改成10还支付成功了——这就是“重放攻击”。数字签名只能保证参数没被篡改,但不能防止黑客重复发送已经签名的请求。加timestamp的作用是限制请求的有效时间(比如5分钟内有效),要是请求的timestamp超过5分钟,服务器就直接拒绝; 最好再加个nonce(随机字符串),把用过的nonce存到Redis里,这样就算timestamp没过期,重复的nonce也会被拒绝。文章里的数字签名代码已经提到了这个点,你抄的时候记得加上这两个参数,彻底堵上重放攻击的漏洞。
抄crypto函数的代码时,参数顺序错了怎么办?
我太懂这种崩溃了!去年帮朋友改代码时,他把pbkdf2Sync的password和salt顺序搞反了,结果生成的哈希和数据库里的对不上,查了3小时才发现问题。crypto模块的很多函数参数顺序很严格,比如pbkdf2Sync的顺序是“密码、盐值、迭代次数、密钥长度、哈希算法”,错一个参数位置就会出问题。解决办法很简单:抄代码时,用变量名明确标注每个参数。比如把代码写成crypto.pbkdf2Sync(userPassword, userSalt, 100000, 64, ‘sha512’),这样就算顺序错了,你一眼就能看出来哪个参数放错了。文章里的代码都用变量名标注了参数,直接抄就行,不用怕顺序错。