MD5、BCrypt、RSA这三个词同时出现在一份技术方案里的时候,往往意味着讨论已经从“怎么存密码”聊到了“怎么传数据”。在社区里我经常看到类似的提问:数据库里的密码用MD5加密安全吗?既然MD5不可逆,为什么不直接用RSA加密存密码?这些问题每一次都能引发争论,但很多人到最后也没搞明白一个关键前提:它们根本不是同一类东西,甚至不能说都是“加密”。
这篇内容我打算按实际项目里的思考路径来讲。先分清三种算法的定位,再分别聊密码存储和数据传输里的选型逻辑,最后把我踩过的坑和排查经验整理出来。如果你正在做登录模块、接口对接或者内部系统的安全改造,这篇文章应该能帮你少走不少弯路。
1. 先给三种算法“对号入座”:散列、加盐哈希、非对称加密
很多人把MD5、BCrypt、RSA放在一起比谁更安全,这个出发点本身就是错的。它们解决的是不同层面的问题,就像你不能问“螺丝刀和电钻哪个更安全”一样,得先看你要拧什么。
1.1 MD5和BCrypt不是“加密”,而是“散列”
MD5的全称是Message-Digest Algorithm 5,输出固定128位,也就是通常看到的32位十六进制字符串。它做的事情是把任意长度的输入变成固定长度的“指纹”。问题是这个指纹不可逆——理论上你无法从指纹反推出原始内容,所以很多人就把它当成“加密”了。
但“不可逆”不等于“加密”。加密的本质是存在解密过程,加密和解密是一对操作;而散列函数没有对应的“解哈希”函数。你可以把散列理解为文档上的骑缝章,盖章很容易,但你不可能从印章还原出整份文档。如果文件换了一个字,盖章后的指纹就会完全不一样。
BCrypt同样是散列算法,但它比MD5特殊得多。BCrypt的设计目标就是“让密码散列变慢”,而且它内置盐值机制,每次计算同一个密码都会得到不同的结果。这样做是为了防止彩虹表和暴力破解。严格来说,BCrypt更应该叫“密码散列算法”,专门用于口令存储。
1.2 RSA是“非对称加密”,解决的是传输问题
RSA是典型的非对称加密算法。所谓非对称,指的是加密密钥和解密密钥是不一样的:公钥负责加密,私钥负责解密;反过来,私钥可以签名,公钥可以验签。它和MD5、BCrypt完全不同,因为RSA是可逆的——只要有对应的私钥,密文是可以还原成明文的。
为什么需要非对称?因为对称加密在密钥分发上有天然的麻烦。想象一下你用一把锁锁住箱子,这把锁的钥匙对方也得有一把,怎么安全地把钥匙送过去就成了问题。RSA的思路是发布一把“挂锁”给全世界,任何人都可以把信息锁进去,但只有持有对应“钥匙”的人才能打开。这把挂锁就是公钥,钥匙就是私钥。
所以从分类上看,MD5和BCrypt属于散列家族,RSA属于加密家族。它们各自解决的场景也不同:密码存储用散列,数据传输用加密,身份验证靠签名。这个底层认知不建立起来,后面所有的选型都容易跑偏。
1.3 一句话记住三者的定位
我用一个很简单的口诀帮团队成员记:“存储用散列,传输用加密,防篡改用签名。” MD5和BCrypt适合做存储时的密码摘要,RSA适合做数据加密和签名。两者不是替代关系,而是互补关系。很多方案里它们甚至会同时出现:登录密码用BCrypt存,登录后的会话凭证用RSA签名,两者并不冲突。
2. 密码存储实战:为什么MD5越来越不受待见,BCrypt成了默认选择
如果你去看十年前的老系统,数据库里的密码字段大概率是MD5值,甚至还有直接明文存的。现在我们当然知道这些做法很危险,但它背后有个很重要的原因:当年的开发环境里,MD5是最容易获得、性能最好的散列函数。问题是,“性能最好”恰恰成了它在密码存储场景下的致命伤。
2.1 MD5的三大致命问题
第一个问题是太快。现代GPU每秒可以执行数十亿次MD5计算。这意味着一个8位纯数字密码,用普通显卡跑,几分钟内就能把所有组合算完;一台专用设备甚至能把MD5的暴力破解速度推到每秒百亿次以上。密码学里有一个基本规律:散列函数越快,密码破解越容易。速度是散列函数的好特性,但在密码存储场景里就是灾难。
第二个问题是碰撞。2004年前后,公开研究已经证明攻击者可以构造两个不同内容拥有相同MD5值的情况。这意味着在某些安全场景里,攻击者可以用一个恶意文件冒充合法文件。虽然碰撞攻击对密码存储的影响不如彩虹表直接,但它已经让MD5背上了“密码学上不安全”的标签。今天的主流安全库基本默认禁用MD5用于签名和证书校验。
第三个问题是盐的缺失。很多旧系统直接用md5(password)存储,两个相同密码会有相同的MD5值。攻击者拿到数据库以后,把所有MD5值排序一下,就能立刻发现哪些用户用了相同密码;再配上预计算的彩虹表,弱密码基本上等同明文暴露。即使后来有人给MD5加了固定盐,只要盐值写在代码里,一旦代码泄露,彩虹表可以重新生成,防护形同虚设。
有人可能会说:那我用循环一万次的MD5行不行?答案是可以延缓,但不够好。因为MD5的内存占用极少,即使迭代一万次,GPU依然可以大规模并行计算。相比之下,BCrypt和Argon2这类算法在设计上就要求占用一定内存并强制串行计算,这才真正提高暴力破解的成本。
2.2 BCrypt的工作原理:慢,就是它的灵魂
BCrypt基于Blowfish加密算法改进而来。它的核心思路是对密码做多次“密钥扩展”运算,次数可以是2的整数次幂,这个指数就是cost参数。cost=12就代表进行4096次扩展运算,每次扩展都混合盐值和密码内容,最终生成一个60字符的散列值。
BCrypt的散列值格式很值得注意,它长这样:
$2b$12$Kx9XoZIiLC2afh8fTj6CQeXoHYP8O0NzYLlU2mQ3WmAwO3L8AqhCq这三个组成部分分别是算法版本、cost值、22字符盐值和31字符散列值。也就是说,BCrypt把盐值和cost都直接嵌进了输出字符串里。验证密码时,只需要从存储字符串中解析出盐值和cost,再重新计算比对即可。这个设计带来的好处是,你不需要额外维护一列盐值字段,也不用担心忘记当年用的是哪个cost参数。
同样的密码,使用不同随机盐值,每次产生的BCrypt结果都不同。但验证时只要拿到存储的散列字符串,它自己就能独立完成校验。这一点对多用户、多系统合并的场景特别友好。
2.3 cost参数怎么选才合理
这是我在实际项目里被问得最多的问题。选cost本质上是平衡安全性和用户体验:cost太高,服务器CPU压力大,接口延迟明显;cost太低,又失去了慢哈希的意义。
我的习惯是从12起步。如果你用的密码库对BCrypt实现有优化,cost=12在普通服务器上大约耗时80到150毫秒;如果机器性能较差或者登录接口并发量很高,可以先压测评估。如果单次登录超过300毫秒,用户可以感知到明显卡顿,就该考虑降低到11,或者迁移到Argon2id。
还有一个容易被忽略的问题:BCrypt计算是CPU密集型的,在Node.js这类单线程环境里,同步调用BCrypt会直接阻塞事件循环。我在某项目里见过原本好好的登录接口,引入BCrypt后高并发时动不动就超时,最后把计算丢到线程池里才解决。如果你用Python或Java,也要留意框架的线程模型,避免密码计算占满所有工作线程。
2.4 存量MD5密码如何平滑迁移
老系统不可能说砍就砍,存量用户的MD5密码也不可能一个个重置。这里可以用“版本化存储”的方式过渡。密码字段增加前缀,比如{MD5}和{BCRYPT},新注册用户直接存BCrypt,老用户先保持MD5。
用户登录时,如果发现存储格式是{MD5},就用MD5验证。验证通过后,立刻用BCrypt重新计算这个密码,并更新到数据库,同时把前缀改成{BCRYPT}。这一步是平滑迁移的核心:不需要用户修改密码,只要他们正常登录一次,就自动升级到新方案。等存量用户迁移比例到可接受水平,比如超过90%,就可以强制剩余用户走一次密码重置。
这里要特别提醒:迁移的时候不要试图用“MD5值”作为输入去生成BCrypt,比如bcrypt(md5(password))这种。因为攻击者拿到数据库后,可以先把MD5值还原成彩虹表结果,再拿这个结果继续套BCrypt。正确做法是拿用户输入的明文密码直接做BCrypt验证,验证成功后再重写为BCrypt格式。
3. 数据传输实战:RSA的正确打开方式是混合加密
聊完密码存储,再来看数据传输。很多人一听到安全传输就想到“用RSA加密”,这个直觉不完整。RSA很好,但拿它直接加密大段业务数据,几乎是所有自制加密方案翻车的地方。
3.1 为什么不能拿RSA直接加密大报文
RSA是公钥加密,确实可以保护数据机密性,但它有三个天然限制:慢、密文膨胀、单次加密长度受限。
先说速度。RSA的加密和解密涉及大数模幂运算,处理1KB数据就要进行多次这样的计算,性能远低于AES。AES在普通CPU上可以做到GB级吞吐,RSA却只能到MB甚至KB级。Web应用里每个请求动辄几KB到几MB,直接用RSA加密完全没有可行性。
再说长度。RSA的加密块长度受到密钥位数和填充方式的限制。以最常见的RSA-2048为例,密钥是256字节:如果用PKCS#1 v1.5填充,单次最多加密245字节;如果用OAEP填充,单次最多加密190字节左右。这还是在SHA-256做哈希的前提下。也就是说一次加密操作只能保护一小块数据,超过这个长度就必须分块处理。
我在某项目里就踩过这个坑。当时要把一段JSON加密传给第三方,直接用库函数一调,报错“data too large”。我第一反应是换个更大的密钥,换成3072位以后发现能多塞一点,但还是只够一次加密几个字段。最后改成混合加密方案才彻底解决问题。
3.2 混合加密的标准流程
混合加密的思路很简单:用对称加密算法加密实际数据,用RSA加密对称算法的密钥。RSA在这里不再承担加密大数据的任务,而是充当“保护钥匙”的角色。
具体流程是:客户端每次请求时生成一个随机的AES会话密钥,用AES加密业务数据得到密文;然后用服务端公钥加密这个AES会话密钥;最后把“加密过的业务数据”和“加密过的AES密钥”一起发送给服务端。服务端收到后,用自己的RSA私钥解出AES密钥,再用AES密钥解出业务数据。
这样做的好处非常明显:AES加密解密速度极快,RSA只加密一个几十字节的密钥,性能和安全性都兼顾。密钥每次会话随机生成,即使某次通信被截获,攻击者拿到的也只是这次的密文和加密后的AES密钥,没有私钥就无法解开。
实际项目里,很多API网关、开放平台、离线交付文件加密,用的都是这类混合方案。如果你听到“RSA加密传输”,大概率背后也是这种“RSA+AES”的组合,而不会是单纯的RSA直接加密。
3.3 数字签名:RSA的另一个不可替代的角色
RSA除了加密,还有一个重要用途是数字签名。签名用的密钥对和加密正好相反:私钥签名,公钥验证。发送方用私钥对一段数据生成签名,接收方用公钥验证签名是否匹配。这个过程不能解密数据,但能确保数据没有被篡改,同时证明数据确实来自持有私钥的一方。
我参与过的一个开放接口项目,就是靠RSA签名做防篡改。双方约定好签名规则,请求参数加上时间戳和随机值,拼接后做RSA私钥签名,接收方用公钥验签。这样即使请求被恶意截获,攻击者没有私钥就无法伪造签名,修改任何一个参数都会导致验签失败。
这里也要提醒一句:签名和加密不能混用。有人会把“加密”和“签名”概念混在一起,用公钥加密数据就当防篡改了。实际上公钥加密是保护机密性,签名才是保证完整性和身份。如果既要机密性又要防篡改,标准做法是“先签名后加密”或者“加密的同时附带签名”。
3.4 密钥管理是传输安全的地基
密钥管理做得再到位,传输方案再精巧,私钥泄露全部白搭。RSA私钥必须放在服务端,而且要做好权限控制。我见过有人把私钥文件直接提交到代码仓库里的,也有人把私钥硬编码在配置文件里当成默认值的,这些都是高危操作。
正确做法是使用环境变量、密钥管理系统或者安全存储服务来管理私钥。私钥文件权限要限制到最小,尽量做到只有运行服务的进程能读取。公钥虽然可以公开,但在项目实践中也建议定期轮换,降低长期密钥泄露带来的风险。RSA密钥长度方面,2025年已经不建议再用1024位,新项目至少2048位起步,敏感场景直接上3072或4096。
4. 最容易翻车的四个细节与排查经验
这部分内容是我在帮团队做代码审查和技术支持时总结出来的高频问题。每一个都在真实项目里出现过,而且往往不是大问题,却能让整个安全方案失效。
4.1 把“散列”当“加密”,导致数据无法还原
最典型的案例:某内部系统需要存储用户的手机号和邮箱,为了方便后面做统计,开发用了MD5存储。等到产品要求“在后台显示明文手机号”的时候,发现问题了——MD5是散列,根本解不出来。最后只能让所有用户重新填写,或者保留一份明文数据库,风险反而更大。
这个坑的本质是没搞清楚散列和加密的区别。散列用于密码校验、数据指纹、防篡改;加密用于需要还原的数据。凡是业务上需要“读回来”的字段,一律不能散列存储。如果既要安全存储又要支持查询,应该用加密算法,比如AES,而不是MD5。
4.2 BCrypt的72字节密码截断问题
BCrypt有一个隐蔽的限制:它只处理输入密码的前72字节。如果用户设置的密码超过72字节,超过的部分会被静默忽略。两个前72字节相同、后面完全不同的密码,在BCrypt验证时都会被当成同一个密码。
我在某项目中遇到过这种问题:用户设置了一个很长的密码,发现用一个“相似但不同”的长密码也能登录成功。排查半天才知道是BCrypt截断。
解决方案有两种:一是在注册时限制密码长度,比如最多72字节,同时明确提示;二是做预散列处理,比如先把密码做SHA-256,拿到32字节的摘要,再对摘要做BCrypt。第二种方式需要注意,不能直接用十六进制字符串再截断,要统一编码规则,否则不同语言算出来的预散列会不一致。由于涉及安全细节,强烈建议在方案设计阶段找有经验的人确认,不要自己拍脑袋。
4.3 RSA填充模式不一致导致的跨系统解密失败
RSA的填充方式和算法本身一样重要。常见的有PKCS#1 v1.5和OAEP,每个库的默认值还不一样。A系统用老版本库加密,B系统用新版本库解密,如果不检查填充模式,就会出现“昨天还能解密,今天突然报错”的诡异现象。
解决办法是,在跨系统方案设计时把填充模式明确写入接口文档。例如“加密使用RSA/ECB/OAEPWithSHA-256AndMGF1Padding”,这句话每个字都有意义。测试阶段也建议准备一组固定向量,在不同系统间互相验签和加解密,确认兼容后再上生产。
4.4 固定盐依然危险
有些开发意识到不能直接用MD5,于是用“用户名+固定盐”拼接后做MD5。这种做法比裸MD5好,但远远不够。固定盐一旦被代码泄露,攻击者可以针对这个盐重新生成彩虹表,和没有盐几乎没有区别。更糟糕的是,如果系统里有大量用户,所有相同密码的用户仍然会产生相同散列值,攻击者一眼就能看出群体特征。
正确做法是每个用户随机生成盐,盐值和散列值一起存储。BCrypt和Argon2这类算法会自动做这件事,这也是我强烈建议直接用成熟算法而不是自己拼盐的原因。自己实现密码哈希,几乎不可避地会出错。
5. 三种算法的一句话选型与项目落地清单
到了可以整理结论的时候。前面讲了很多原理,这里我用一张表把三种算法的差异固定下来,方便你直接复制到方案文档里。
| 对比项 | MD5 | BCrypt | RSA |
|---|---|---|---|
| 算法类别 | 散列算法 | 加盐密码散列算法 | 非对称加密算法 |
| 是否可逆 | 不可逆 | 不可逆 | 可逆(持有私钥时) |
| 输出长度 | 32位十六进制 | 60字符 | 密文长度与密钥长度相关 |
| 单次加密数据量 | 任意长度输入 | 有72字节密码限制 | 实际上限几百字节 |
| 速度 | 极快 | 故意设计为慢 | 慢 |
| 是否自动加盐 | 否 | 是 | 不适用 |
| 典型用途 | 非安全完整性校验、旧系统兼容 | 密码存储、口令校验 | 密钥交换、数字签名、小数据加密 |
| 强烈不建议 | 密码存储、证书签名 | 不适用(密码存储首选) | 大数据量直接加密 |
这套表在我评审技术方案时经常用。如果对方说“我用MD5存密码”,我基本能判断他没搞清楚散列和加密的关系;如果对方说“我用RSA加密大文件”,我也知道他没有考虑性能和数据长度限制。
具体到落地场景,我的做法是这样的:
- 密码存储:新系统优先考虑Argon2id,如果团队已经熟悉BCrypt且没有特殊合规要求,BCrypt也完全可用。千万不要再用MD5、SHA-1这类快速散列做密码存储。
- 数据传输:Web场景优先使用标准安全传输协议,应用层不要重造加密协议。离线文件、开放平台API这类需要端到端加密封装时,用RSA+AES混合加密方案。
- 完整性校验:非涉密文件的校验,MD5虽然还能用,但我更推荐SHA-256。涉及签名、证书、防伪造场景,必须用RSA或ECDSA这类签名算法。
- 密钥管理:私钥永远不能进代码仓库,密钥长度不低于2048位,密码学参数写在接口文档里并保持跨系统一致。
- 日志纪律:任何情况下都不允许把明文密码打印到日志里,这是最低底线。
我在实际排查过太多“明明用了加密,还是被拖库”的案例。一条一条追下去,问题通常很简单:要么用了MD5,要么盐值是固定的,要么私钥权限没管好。密码学和加密工具用错了地方,再高级的算法也救不回来。
所以我最后想分享的经验是:做安全选型的时候,不要只问“哪个算法更强”,而要先问“我到底在防谁、防什么场景”。密码存储面对的是数据库泄露后离线暴破的问题,所以需要慢哈希加盐;数据传输面对的是信道窃听和中间人篡改的问题,所以需要加密加签名。把目标想清楚了,MD5、BCrypt、RSA各归其位,方案自然就顺了。