所有分类
  • 所有分类
  • 游戏源码
  • 网站源码
  • 单机游戏
  • 游戏素材
  • 搭建教程
  • 精品工具

Node.js crypto模块实现示例:哈希/加密/解密/签名保姆级代码,新手直接抄

Node.js crypto模块实现示例:哈希/加密/解密/签名保姆级代码,新手直接抄 一

文章目录CloseOpen

别慌,这篇文章就是为你准备的!我们把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个功能的代码都整理好了,每段代码都能直接运行,连踩过的坑都帮你填上了。

  • 哈希:用户密码别存明文,用SHA256加salt才安全
  • 去年帮朋友的美食博客做用户登录功能时,他一开始直接把用户密码存明文——我看到数据库里的“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模式的坑
  • 做电商项目时,我需要加密用户的收货地址和手机号——一开始用了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丢了,加密的数据就永远解不开了!

  • 非对称加密:RSA传敏感数据,公钥加密私钥解密才安全
  • 去年做金融项目时,需要传输用户的银行卡号——如果用对称加密,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,这样既安全又高效。

  • 数字签名:接口参数防篡改,用RS256签名才靠谱
  • 去年做支付接口时,我遇到过一个大问题:测试用工具篡改了订单金额(把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&timestamp=1620000000)

    const requestParams = 'userId=123&amount=100&timestamp=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——去年我帮朋友改代码时,他把saltpassword的顺序搞反了,结果生成的哈希和数据库里的对不上,查了3小时才发现。

    解决办法:抄代码时,用变量名明确标注参数,比如:

    // 用变量名标注参数,就算顺序错了也能一眼看出来
    

    const hashed = crypto.pbkdf2Sync(

    userPassword, // 密码

    userSalt, // 盐值

    100000, // 迭代次数

    64, // 密钥长度

    'sha512' // 哈希算法

    );

  • 缓冲区处理要注意,否则会得到乱码
  • crypto模块的很多函数返回的是Buffer类型(比如crypto.randomBytes(16)返回的是16字节的Buffer),如果你直接用toString()转字符串,可能会得到乱码——因为Buffer默认用utf8编码,而密文、哈希值不是utf8格式的。

    解决办法:转字符串时一定要用hexbase64编码,比如:

    // 正确的转码方式:用hex或base64
    

    const salt = crypto.randomBytes(16).toString('hex'); // 转成hex字符串

    const encrypted = cipher.final().toString('base64'); // 转成base64字符串

  • 算法名称别写错,否则会报“Invalid algorithm”
  • 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’),这样就算顺序错了,你一眼就能看出来哪个参数放错了。文章里的代码都用变量名标注了参数,直接抄就行,不用怕顺序错。

    原文链接:https://www.mayiym.com/50636.html,转载请注明出处。
    0
    显示验证码
    没有账号?注册  忘记密码?

    社交账号快速登录

    微信扫一扫关注
    如已关注,请回复“登录”二字获取验证码