这道题看似简单,其实是一道送命题,很多人尤其是一些新入门的同学会觉得,安全啊,MD5 首先是加密的字符串,其次是不可逆的,所以它一定是安全的。如果你这样回答,那么就彻底掉进面试官给你挖好的坑了。
为什么呢?因为答案是“不安全”,而不是“安全”。
MD5 之所以说它是不安全的,是因为每一个原始密码都会生成一个对应的固定密码,也就是说一个字符串生成的 MD5 值是永远不变的。这样的话,虽然它是不可逆的,但可以被穷举,而穷举的“产品”就叫做彩虹表。
彩虹表是一个用于加密散列函数逆运算的预先计算好的表, 为破解密码的散列值(或称哈希值、微缩图、摘要、指纹、哈希密文)而准备。 一般主流的彩虹表都在 100G 以上。这样的表常常用于恢复由有限集字符组成的固定长度的纯文本密码。这是空间/时间替换的典型实践,比每一次尝试都计算哈希的暴力破解处理时间少而储存空间多,但却比简单的对每条输入散列翻查表的破解方式储存空间少而处理时间多。
简单来说,彩虹表就是一个很大的,用于存放穷举对应值的数据表。 以 MD5 为例,“1”的 MD5 值是“C4CA4238A0B923820DCC509A6F75849B”,而“2”的 MD5 值是“C81E728D9D4C2F636F067F89CC14862C”,那么就会有一个 MD5 的彩虹表是这样的:
原始值 | 加密值 |
---|---|
1 | C4CA4238A0B923820DCC509A6F75849B |
2 | C81E728D9D4C2F636F067F89CC14862C |
... | ... |
大家想想,如果有了这张表之后,那么我就可以通过 MD5 的密文直接查到原始密码了,所以说数据库如果只使用 MD5 加密,这就好比用了一把插了钥匙的锁一样不安全。
想要解决以上问题,我们需要引入“加盐”机制。
盐(Salt):在密码学中,是指通过在密码任意固定位置插入特定的字符串,让散列后的结果和使用原始密码的散列结果不相符,这种过程称之为“加盐”。
说的通俗一点“加盐”就像炒菜一样,放不同的盐,炒出菜的味道就是不同的,咱们之前使用 MD5 不安全的原因是,每个原始密码所对应的 MD5 值都是固定的,那我们只需要让密码每次通过加盐之后,生成的最终密码都不同,这样就能解决加密不安全的问题了。
加盐是一种手段、是一种解决密码安全问题的思路,而它的实现手段有很多种,我们可以使用框架如 Spring Security 提供的 BCrypt 进行加盐和验证,当然,我们也可以自己实现加盐的功能。
本文为了让大家更好的理解加盐的机制,所以我们自己来动手来实现一下加盐的功能。
实现加盐机制的关键是在加密的过程中,生成一个随机的盐值,而且随机盐值尽量不要重复,这时,我们就可以使用 Java 语言提供的 UUID(Universally Unique Identifier,通用唯一识别码)来作为盐值,这样每次都会生成一个不同的随机盐值,且永不重复。
加盐的实现代码如下:
import org.springframework.util.DigestUtils; import org.springframework.util.StringUtils; import java.util.UUID; public class PasswordUtil { /** * 加密(加盐处理) * @param password 待加密密码(需要加密的密码) * @return 加密后的密码 */ public static String encrypt(String password) { // 随机盐值 UUID String salt = UUID.randomUUID().toString().replaceAll("-", ""); // 密码=md5(随机盐值+密码) String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes()); return salt + "$" + finalPassword; } }
从上述代码我们可以看出,加盐的实现具体步骤是:
那么,问题来了,既然每次生成的密码都不同,那么怎么验证密码是否正确呢?
要验证密码是否正确的关键是需要先获取盐值,然后再使用相同的加密方式和步骤,生成一个最终密码和和数据库中保存的加密密码进行对比,具体实现代码如下:
import org.springframework.util.DigestUtils; import org.springframework.util.StringUtils; import java.util.UUID; public class PasswordUtil { /** * 加密(加盐处理) * @param password 待加密密码(需要加密的密码) * @return 加密后的密码 */ public static String encrypt(String password) { // 随机盐值 UUID String salt = UUID.randomUUID().toString().replaceAll("-", ""); // 密码=md5(随机盐值+密码) String finalPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes()); return salt + "$" + finalPassword; } /** * 解密 * @param password 要验证的密码(未加密) * @param securePassword 数据库中的加了盐值的密码 * @return 对比结果 true OR false */ public static boolean decrypt(String password, String securePassword) { boolean result = false; if (StringUtils.hasLength(password) && StringUtils.hasLength(securePassword)) { if (securePassword.length() == 65 && securePassword.contains("$")) { String[] securePasswordArr = securePassword.split("\\$"); // 盐值 String slat = securePasswordArr[0]; String finalPassword = securePasswordArr[1]; // 使用同样的加密算法和随机盐值生成最终加密的密码 password = DigestUtils.md5DigestAsHex((slat + password).getBytes()); if (finalPassword.equals(password)) { result = true; } } } return result; } }
只是简单的使用 MD5 加密是不安全的,因为每个字符串都会生成固定的密文,那么我们就可以使用彩虹表将密文还原出来,所以它不是安全的。想要解决这个问题,我们需要通过加盐的手段,每次生成一个不同的密码,就把这个问题解决了。