看了很多简历,密码加密这块基本用的都是 MD5 ,应该是看的教程都是这么讲的。实际项目中,是不建议这么做,存在安全风险。

哈希算法可以简单分为两类:

  1. 加密哈希算法:安全性较高的哈希算法,它可以提供一定的数据完整性保护和数据防篡改能力,能够抵御一定的攻击手段,安全性相对较高,但性能较差,适用于对安全性要求较高的场景。例如 SHA2、SHA3、SM3、RIPEMD-160、BLAKE2、SipHash 等等。

  2. 非加密哈希算法:安全性相对较低的哈希算法,易受到暴力破解、冲突攻击等攻击手段的影响,但性能较高,适用于对安全性没有要求的业务场景。例如 CRC32、MurMurHash3、SipHash 等等。

除了这两种之外,还有一些特殊的哈希算法,例如安全性更高的慢哈希算法

MD 算法有多个版本,包括 MD2、MD4、MD5 等,其中 MD5 是最常用的版本,它可以生成一个 128 位(16 字节)的哈希值。从安全性上说:MD5 > MD4 > MD2。除了这些版本,还有一些基于 MD4 或 MD5 改进的算法,如 RIPEMD、HAVAL 等。即使是最安全 MD 算法 MD5 也存在被破解的风险,攻击者可以通过暴力破解或彩虹表攻击等方式,找到与原始数据相同的哈希值,从而破解数据。

为了增加破解难度,通常可以选择加盐。盐(Salt)在密码学中,是指通过在密码任意固定位置插入特定的字符串,让哈希后的结果和使用原始密码的哈希结果不相符,这种过程称之为“加盐”。加盐之后就安全了吗?并不一定,这只是增加了破解难度,不代表无法破解。而且,MD5 算法本身就存在弱碰撞(Collision)问题,即多个不同的输入产生相同的 MD5 值。因此,不建议使用 MD5 加密密码,即使加盐也存在安全风险。

即使到今天,网上依然还有很多教程采用 MD5 + Salt 的方式加密密码,需要重点注意一下!

为了增加安全性,可以使用安全性较高的加密哈希算法+ Salt(盐)(例如 SHA2、SHA3、SM3,更高的安全性更强的抗碰撞性)。建议每个用户的 Salt 值不同(最好对不同用户的密码随机生成不同的 Salt,Salt 库和密码库分离开),这样就没办法用彩虹表进行批量破解。不过,这不代表没有破解风险了(利用密码破解硬件,我们可以在一秒钟内进行数十亿次的哈希计算)。

安全性更高的一种方案是使用 密钥派生算法(Key Derivation Function,简称 KDF,也称为密码哈希算法)。相比其他加密哈希算法,KDF 具有一个独特属性——计算速度很慢,而且从设计上就使其计算速度难以提升,所以 KDF 也被称为 慢哈希算法 。这个慢相比于其带来的安全性来说是可以接受的,毕竟主要也是在登录时执行一次。

常见的 KDF 算法主要有(安全程度依次递增):

  1. PBKDF2:其核心是对 HMAC 进行多次迭代以增加破解难度。Bcrypt 对内存的要求较低,并不能抵抗密码破解硬件(如 GPU、ASIC、FPGA)攻击。这个 KDF 算法比较老了,目前已经不推荐使用。

  2. Bcrypt:一种基于 Blowfish 加密算法的密码哈希算法,专门为密码加密而设计,安全性高于 PBKDF2。Bcrypt 对内存的要求较低,同样不能抵抗密码破解硬件攻击。

  3. Scrypt:相比于 PBKDF2 和 Bcrypt,其占用的内存更多,安全性也要更高。它还可以通过调整内存和 CPU 的使用量来增加破解的难度。

  4. Argon2:目前最强的密码 Hash 算法,在 2015 年赢得了密码 Hash 竞赛。和 Scrypt 一样,Argon2 同样需要大量的内存。二者综合使用加盐、多次迭代、大量消耗 CPU 时间和内存资源等手段,大大提升了对抗密码破解硬件的能力。

Spring Security 提供了这些 KDF 算法的实现:点击跳转

Spring Security 提供的 KDF 算法实现

对于绝大部分项目来说,个人觉得 Bcrypt 就足够了,虽然它的安全性比不上 Scrypt 和 Argon2,但综合起来性价比较高。

这里再单独介绍一下 Bcrypt:Bcrypt 采用了 salt(盐) 和 cost(成本) 两种机制,它可以有效地防止彩虹表攻击和暴力破解攻击,从而保证密码的安全性。加 salt 可以防止彩虹表攻击,也就是说,使用 Bcrypt 加密密码时已经包含了一个随机加盐的过程,不需要额外加盐了。cost 又称为工作因子,定义了哈希计算的复杂度。成本越高,计算所需的时间和资源就越多,这使得暴力破解攻击变得更加困难。实际项目中,可以根据系统的性能和安全需求调整 cost。

Spring Security 提供的BCryptPasswordEncoder 工作因子范围在 4-31 ,默认是 10。

 /**
  * @param strength the log rounds to use, between 4 and 31
  */
 public BCryptPasswordEncoder(int strength) {
  this(strength, null);
 }