ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

MD5、BCrypt、RSA三者区别:密码存储与数据传输如何正确选型?

MD5、BCrypt、RSA三者区别:密码存储与数据传输如何正确选型? 前一阵做代码评审看到某业务模块的用户表里password字段直接存的是MD5值加盐都没有。问同事为什么这么写他的回答特别典型“这不是加密了吗别人拿到也解不开啊。”这个说法我听过太多次了。MD5、BCrypt、RSA这三个词在很多人嘴里都是“加密”但实际上它们解决的问题完全不同用错场景的后果也比想象中严重得多。今天就把这三个东西彻底聊透MD5本质是什么、为什么它不适合存密码BCrypt凭什么成为当前密码存储的主流方案RSA在数据传输里到底是怎么工作的以及实际项目中到底该怎么选型。1. 三种加密的本质差异摘要、慢哈希与非对称有些概念一混后面的坑就全踩了。先明确一个大前提MD5不是加密算法。MD5的全称是Message-Digest Algorithm 5中文叫消息摘要算法第五版它属于哈希函数也叫散列函数。哈希函数的特征是有输入就能算出固定长度的输出但反过来从输出推输入在计算上不可行。比如输入“123456”MD5计算结果是“e10adc3949ba59abbe56e057f20f883e”这个过程只要几十纳秒但给你这段摘要你想反推出“123456”除了暴力穷举没有任何高效算法。从这个角度说MD5确实“解不开”但它的问题是可预测、可穷举、可查表。BCrypt则不一样。它也是一种哈希算法但它是专门为密码存储设计的自适应哈希函数核心特点是“慢”。它的慢不是缺陷而是安全策略它通过内置的cost factor成本因子主动控制计算耗时让每一次哈希计算都需要几十毫秒甚至几百毫秒。一个普通用户登录时多等几十毫秒完全无感但攻击者想批量破解几十万条密码时总耗时会被拉长到不可接受的程度。RSA更特殊它属于非对称加密是真正意义上的“可逆加密”。这里要提一个核心概念加密算法必须是可逆的。加密的本质是在密钥控制下把明文变成密文持有解密密钥的人可以把密文还原成明文。MD5和BCrypt都做不到这一点它们是不可逆的单向函数。RSA的不可逆性只对没有私钥的人成立持有私钥的一方可以完整解出原始数据。这一点决定了RSA的应用方向它解决的是数据保密传输问题而不是密码存储问题。所以一张表就能说清它们的定位算法类型是否可逆核心用途典型场景MD5消息摘要不可逆完整性校验文件校验、接口签名建议用更强算法替代BCrypt自适应哈希不可逆密码存储用户登录密码的落库与校验RSA非对称加密可逆凭私钥数据机密性密钥交换、敏感数据加密传输MD5适合的场景是校验数据有没有被篡改BCrypt适合的场景是密码验证RSA适合的场景是保证传输过程中数据不被窃听。三者不是同一个赛道上的竞争对手而是完全不同的三类工具。2. 为什么不能拿MD5存密码从一次模拟“拖库”事故说起很多团队还在用MD5表面上是技术选型问题本质上是对攻击成本没有概念。我把一次真实的模拟演练过程拆开来讲你就能直观感受到MD5存密码有多危险。2.1 哈希速度是MD5的致命伤我用一台普通的办公笔记本电脑做过测试纯CPU环境跑MD5每秒大约能算400万次。看这个数字可能没概念换一个说法假设攻击者拿到了一整库的用户密码摘要随便选一条用最常见的弱口令字典跑一遍也就是几百万个候选密码逐一做MD5计算然后比对摘要。整个过程不到一秒钟就能把字典里所有组合试完如果这条密码恰好是弱口令当场就能命中。更可怕的是数字升级能力。普通电脑上用支持MD5的专用破解设备或者GPU加速库每秒可以跑到几十亿次甚至上百亿次。在这个速度面前8位以内纯数字的密码组合1秒内必被扫穿8位小写字母加数字的组合也就撑几个小时的暴力穷举。这根本不是“够不够安全”的讨论而是“能不能撑过当天晚上”的问题。反观BCrypt在同一台电脑上跑一次计算大约需要80毫秒换算下来每秒只能算十来次。就算攻击者拿到库里所有摘要想要把字典跑完时间代价会放大几十万倍。这就是“慢哈希”的核心价值让攻击者的算力优势清零。2.2 彩虹表与加盐有人会说“那我给MD5加盐不就行了”加盐确实有效但它解决的是另一个问题彩虹表攻击。彩虹表是预先生成好的“密码→摘要”映射表攻击者可以提前把上亿条常见组合的MD5值算好存起来拿库里的摘要一对就知道密码是什么。加盐之后每条摘要都加入了随机字符串同一个密码在不同用户下会产生不同摘要彩虹表直接失效。但加了盐的MD5仍然有问题。盐通常只增加20-30位随机数据以当前算力攻击者拿到盐之后可以对目标用户做单点穷举。说的直白一点如果某个账号的盐是固定存储的攻击者对这条记录跑字典攻击速度依然是每秒几百万次只不过因为加盐需要重新计算总时间会比无盐时稍长但依然远远不够安全。核心问题始终没有解决MD5太快了。2.3 破解MD5密码的实际路径从一个攻击视角看拿到MD5摘要之后破解流程大概是这样的判断摘要格式32位十六进制字符串基本可以确认是MD5。直接在线查询把摘要丢到现成的MD5反查库中撞库命中率对于弱口令非常高。跑本地字典用rockyou等常见密码字典一条条算MD5比对。暴力穷举对短密码、数字类密码做全量扫描。规则变形在字典基础上加后缀、改大小写、插入生日数字这部分靠哈希速度做支撑。整个过程没有用到任何破解“加密算法”的高级技巧纯粹是算力碾压。MD5的设计目标本来就是速度它诞生于1991年那时候的计算机算力和今天完全不在一个量级。拿30多年前为速度而生的算法来保护今天的密码等同把家门钥匙挂在门牌号下面谁来都能开门进去。注意不要再在新系统里用MD5做密码哈希哪怕是加盐的MD5。判断标准很简单一个密码校验过程在服务端耗时是否达到50毫秒以上。达不到就说明算法选错了。3. BCrypt正确打开方式参数选择与落地配置BCrypt是当前存储密码最稳妥的常见选择之一它从设计之初就假定攻击者算力强大因此故意把计算拉慢。要做到“安全且不那么影响体验”关键在参数。3.1 BCrypt的盐是自动处理的很多人习惯“密码哈希算法(密码盐)”但BCrypt的用法完全不同。BCrypt生成的字符串自带盐而且盐是明文嵌在哈希结果里的。调用BCrypt校验时只需要把存储的哈希字符串和用户输入的密码传进去算法会自动从哈希串中提取盐重新计算再跟存储值比对。一个典型的BCrypt哈希长这样$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy拆开看$2a$是算法版本标识10是cost factor紧接着的22个字符是盐后面的字符串是真正的哈希值。整个过程不需要开发者在数据库里单独开一个salt字段也不需要自己生成随机字符串省事很多。这一点对新手极其友好也减少了一个很容易犯的错误把盐写死在代码里。BCrypt自动管理盐天然规避了这个问题。3.2 cost factor选多少从算力角度拆解cost factor是BCrypt的核心调优参数它决定计算迭代次数。数学关系是迭代次数 2 的 cost 次方。cost10时做1024轮cost12时做4096轮每增加1耗时翻倍。那么应该选多少我惯用的流程是先在本机跑一个基准测试# 以Python为例快速测速 python -c import bcrypt, time for cost in [10, 11, 12, 13]: start time.time() for _ in range(10): bcrypt.hashpw(btest, bcrypt.gensalt(roundscost)) print(cost, (time.time()-start)/10*1000, ms) 不同机器结果不一样大致可以参考我的MBP上cost10时单次哈希约65毫秒cost12时约260毫秒cost14时已经到了秒级。选择标准是服务端高峰期CPU能承受、用户等待可接受即可。个人建议在常规Web项目里从cost10起步压测时如果发现CPU占用过高再上调或下调。有一个反直觉的细节值得注意cost过低时BCrypt和MD5的区别被削弱失去抗暴力破解的意义但cost过高时用户体验会明显变差而且在恶意攻击下攻击者可以大量请求登录接口消耗服务器CPU形成变相的拒绝服务攻击。所以不是越大越好需要平衡。Java里常见的Spring Security配置是这样BCryptPasswordEncoder encoder new BCryptPasswordEncoder(10); String encoded encoder.encode(user_password); boolean matched encoder.matches(user_password, encoded);Python里也很简洁import bcrypt hashed bcrypt.hashpw(buser_password, bcrypt.gensalt(rounds10)) ok bcrypt.checkpw(buser_password, hashed)3.3 BCrypt的输入长度限制与额外防护BCrypt有一个很少被注意到的硬限制它只处理前72字节的输入超过部分会被直接忽略。也就是说如果用户密码超过72字节后面输入再多也不会参与哈希计算。对绝大多数人来说72字节够用了因为普通密码最多二三十个字符但如果你做的是国际化产品允许用户填写很长很复杂的密码甚至把整句话当密码就要注意这个边界。两种做法可以解决一是在加密前直接限制密码最大长度比如最多64字节二是在BCrypt之前先做一次SHA-256再把SHA-256的原始字节丢给BCrypt。第二种方式兼容性不错但要注意必须先取原始字节而不是十六进制字符串否则长度会翻倍依然可能超出限制。另一个实用的经验是不要把“密码是否匹配”和“账号是否存在”的响应区别得太明显。如果登录时“账号不存在”和“密码错误”返回不同的提示攻击者可以借此枚举账号。业界常用的做法是统一提示“账号或密码错误”。虽然BCrypt本身不会泄露账号状态但响应层面的信息泄露往往被忽略。密码存储是一个纵深防御的问题除了选对算法还要注意其他部分密码复杂度策略、登录接口限流、异常登录告警、定期强制更新高危口令。BCrypt解决的是密码落库后“即使拖库也难以破解”的问题但它抗不了弱密码本身也抗不了用户把密码写在便利贴上这种物理泄露。4. RSA与数据传输从密钥对生成到混合加密实践RSA是公钥密码体系的代表它和对称加密的最大区别在于加密和解密用的是两把不同的钥匙一把公开一把私藏。这个特性让它在数据传输场景中显得特别重要。4.1 公钥加密、私钥解密的流程先理解一个最直观的用法。假设服务端要给某个客户端传输一段敏感数据客户端可以先把自己的公钥发给服务端服务端用这个公钥加密数据密文到达客户端之后客户端用私钥解密整个过程中没有任何人能看到明文。这里有个关键点公钥是公开的谁都可以拿到但即使拿到了公钥也无法推出私钥。RSA的安全性建立在“大整数分解”的数学难题之上给定一个大合数要把它分解成两个大质数在当前算力下极其困难。密钥长度越长分解难度指数级上升。以RSA-2048为例要分解一个2048位的整数地球上目前没有任何公开的算力可以在可行时间内完成。这就是RSA能保护传输数据的根基。4.2 密钥长度与数据长度的限制关于RSA有一个常见的认知误区以为RSA和AES一样可以加密任意长度的数据。实际上RSA对明文长度有严格限制。以2048位密钥为例RSA一次能加密的明文最大长度是密钥长度(字节) - 填充占用(字节)如果用PKCS#1 v1.5填充每个块要占11字节的开销那么最大明文长度 2048 / 8 - 11 245字节。如果用OAEP填充推荐SHA-1时开销为42字节最大明文长度是214字节SHA-256时开销更多最大明文190字节左右。超过这个长度就必须把数据进行分段加密每段分别用RSA加密再拼接性能极差。所以真实业务里几乎不会用RSA直接加密业务数据而是采用混合加密。经典的HTTPS握手就是这么做的客户端生成一个临时的对称密钥实际是Pre-Master Secret。客户端用服务器的RSA公钥加密这个临时对称密钥发送给服务器。服务器用RSA私钥解密得到临时对称密钥。后续所有业务数据都用这个对称密钥配合AES算法加密传输。这样做的理由很好理解对称加密AES加解密速度快可以处理任意长度数据但存在密钥分发问题RSA解决密钥分发但速度慢、长度受限。把两者结合各取所长就是日常所有加密通信的基本套路。4.3 RSA的实际代码操作与坑假设你写了一个接口要给第三方传递保单数据又不方便走HTTPS常见做法是约定RSA公钥加密。用Java实现时大致是这样// 读取对方公钥 PublicKey publicKey readPublicKey(publicKeyPemText); Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encrypted cipher.doFinal(plainData);接收方解密PrivateKey privateKey readPrivateKey(privateKeyPemText); Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] decrypted cipher.doFinal(base64Decoded(encrypted));看起来简单但坑非常多。最常见的坑包括第一个坑加密结果要Base64编码。RSA加密后得到的密文是二进制数据直接转成UTF-8字符串必然乱码。必须用Base64做文本化编码传输到对端再解码。很多新手在这个环节卡半天还以为是RSA解不开。第二个坑密钥和加密公钥的格式解析。不同语言生成RSA密钥的格式有差异。Java默认生成的X.509格式公钥可以直接用getPublicKey读但如果对方发的PEM格式带了-----BEGIN PUBLIC KEY-----头就需要先把头尾去掉做Base64解码后再通过KeyFactory生成。第三个坑分段加密问题。上面提到的245字节限制直接作用就是如果业务字段超过这个长度doFinal会抛DataLengthMustBeLessThanModulus异常。很多项目在测试阶段功能正常一上线传了长文本就爆。解决办法就是改用混合加密或者对字段长度做强制校验。第四个坑私钥权限保护。RSA私钥一旦泄露所有用它加密过的历史数据都存在风险因为它可以解出私钥对应的所有密文。私钥文件在生产环境必须做严格权限控制不能放进代码仓库也不能打日志打印。正确的做法是把私钥放在专用密钥管理服务中应用运行时通过接口读取。4.4 推荐的混合加密实现思路如果业务量不大但确实有敏感数据传输需求AESRSA混合加密在应用层的实现思路如下每次加密时随机生成一个AES密钥用AES-GCM加密业务数据得到密文和认证标签。用接收方的RSA公钥加密这个AES密钥。把“加密后的AES密钥”和“AES密文”一起传给接收方JSON字段分别叫encrypted_key和payload。接收方先用RSA私钥解密出AES密钥再用AES密钥解密业务数据。这样做的好处是业务数据大小不受RSA限制加密性能也远远优于纯RSA同时RSA只负责保护体积很小的对称密钥安全性也得到保证。这也是项目里最稳妥的长期方案。5. 常见问题速查与选型决策表写到这里把我在实际项目中遇到的高频问题和决策经验统一整理一下方便大家直接对照。5.1 高频率踩坑问题问题原因解决方案登录校验总是通不过BCrypt哈希里带了盐比对时必须用checkpw/matches不能直接比较哈希字符串确认代码调用的是比对函数而不是字符串相等BCrypt存入数据库报错哈希串长度60个字符部分数据库字段定义为varchar(32)字段长度改成varchar(64)或更长RSA加密后一顿乱码没做Base64编码把二进制密文当字符串传输加密后Base64编码文本化解密前先解码RSA报DataLengthMustBeLessThanModulus单次加密数据超过密钥上限改用混合加密或对字段做长度限制并用分段加密加盐MD5还被破解哈希太快加盐只挡彩虹表挡不住暴力穷举换BCrypt等慢哈希方案别再优化速度密码哈希结果每次都不一样这正是BCrypt的正常行为因为盐随机生成不要慌校验函数内部会自己处理盐5.2 选型时想清楚三件事第一个问题是“数据需要还原吗”。需要还原的选对称加密如AES或非对称加密如RSA不需要还原只做比对的选哈希方案BCrypt优先。第二个问题是“谁要解这个数据”。如果只有接收方自己能解RSA或AES都行如果需要双方各自持有一把不同钥匙非对称加密是唯一选择。第三个问题是“被破解的代价有多大”。用户密码这种一旦泄露就是大范围安全事件的数据必须用BCrypt临时性的、低敏感的接口签名用HMAC-SHA256就够不必上RSA。5.3 关于MD5的一点公正评价说MD5不适合存密码不等于MD5毫无用途。它在完整性校验领域仍然有历史地位比如下载文件时对比MD5值确认文件没有损坏。但即便是完整性校验今天也建议优先用SHA-256或SHA-3因为MD5碰撞问题已经被公开证明攻击者可以构造两个不同内容但MD5相同的文件这在安全攸关的校验场景中是致命的。自2011年之后大部分CA机构就不再信任MD5签名的证书了。说句实在话新写代码时遇到MD5就直接打问号绝大部分场景都有更好的替代方案。真正非MD5不可的现代业务我还没遇到过。6. 实操场景复盘与经验总结去年某项目接手一套老系统用户表里上百万条密码全都是加盐MD5。当时没法一键全量迁移到BCrypt因为BCrypt校验必须重新用密码原文算而原文已经不在了。最后采用渐进迁移策略登录时先拿用户输入的密码先做MD5校验如果MD5比对通过立即用这一次输入的明文密码重新生成BCrypt哈希并更新数据库下一次登录走BCrypt校验。一个多月后活跃用户就全部迁移到了BCrypt。这个方案不用发密码重置邮件也不用改用户逻辑体验平滑到几乎无感。另一件事也和RSA有关。某个对接项目里对方要求用RSA加密请求体结果他们给的公钥是2048位的而我方传的参数却以JSON字符串整体加密明文长度轻松超过200字节。联调时报错之后双方第一反应都是“对方RSA实现有问题”花了不少时间才定位到是数据长度超标。后来我把方案改成请求体先用AES-GCM加密再用RSA加密AES密钥问题就再没出现过。这个经验我见到类似的对接问题就一定会提一遍。这三种算法的选择看似复杂归根到底就是一句话想清楚数据要不要还原、谁需要还原、被还原后的代价有多大。密码存储选慢哈希BCrypt数据传输选非对称加密RSA加上对称加密AES的混合方案完整性校验选SHA-256系。每种算法都有自己的生态位选对了就是安心的保障选错了就是埋雷。最后分享一个我自己的习惯每次接新项目第一件事就是检查现有代码的哈希算法和加密方案看到MD5存密码就标记为高优先级技术债看到RSA分块加密大数据就标记为重构项。这些细节平时不显山不露水一旦出事都是大事故。早点把它修好比任何事后补救都省心。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表