Salt加密算法
Salt(cryptography)
密码加密
md5密码加密
md5是一种单向加密算法,它将任意长度的数据映射为一个固定长度的哈希值。这种算法的特点是不可逆,即无法通过哈希值反推出原始数据。
在过去,md5被广泛用于密码加密,因为它的计算速度非常快,而且具有很高的安全性。但是,随着计算机计算能力的提高,md5已经不再适合用于密码加密,
因为它的碰撞概率已经很高。
md5 是一种不可逆的加密方法,即密码被 md5 加密后是无法解密出原始密码的,验证密码是否正确的方法是将用户输入的密码 md5 加密后
于数据库里保存的 md5 机密后的结果进行比对。这样,服务器端在不知道真实用户密码的情况下也能对用户密码进行验证了。
这是早期比较主流的做法,然而,这依然是非常不安全的。因为只要枚举所有短密码进行 md5 加密,做成一个索引表,就能轻易的逆推出原始密码。这种预先计算好的用于逆推加密散列函数的表就是“彩虹表”。
随着“彩虹表”不断变大,md5 的加密已经变得非常的不安全。2015年10月网易邮箱的用户密码泄露也被怀疑只对密码进行了 md5 加密。
加盐 hash 保存密码
下面详细介绍一下加Salt散列的过程。介绍之前先强调一点,前面说过,验证密码时要使用和最初散列密码时使用“相同的”佐料。所以Salt值是要存放在数据库里的。

图1. 用户注册
如图1所示,注册时,
- 用户提供密码(以及其他用户信息);
- 系统为用户生成Salt值;
- 系统将Salt值和用户密码连接到一起;
- 对连接后的值进行散列,得到Hash值;
- 将Hash值和Salt值分别放到数据库中。

图2. 用户登录
如图2所示,用户登录时,
- 用户提供用户名和密码;
- 系统通过用户名找到与之对应的Hash值和Salt值;
- 系统将Salt值和用户密码连接到一起;
- 对连接后的值进行散列,得到Hash值;
- 将Hash值和数据库中保存的Hash值进行比较,如果相同,则验证成功。
加盐 hash 是指在加密密码时,不只是对密码进行 hash ,而是对密码进行调油加醋,放点盐(salt)再加密,
一方面,由于你放的这点盐,让密码本身更长强度更高,彩虹表逆推的难度更大,也因你放的这点盐,让黑客进行撞库时运算量更大,破解的难度更高。
彩虹表:彩虹表记录了几乎所有字符串的MD5对照表,有了彩虹表MD5就相当于是不存在了,因为一种字符串就只有一种特定的MD5格式。
Salt实现
如何进行加盐就是一门很重要的学问了。md5 是一种 hash 算法,以下就拿 md5 来举例。假如密码是 123456 ,md5 的结果如下:
md5("123456") = e10adc3949ba59abbe56e057f20f883e
像 123456 这样的简单密码,是很容易被逆推出来的。但是假如我们往简单密码里加点盐试试:
md5("123456"+"#g5Fv;0Dvk")=93e00abe0aa46a938cbc6c9856725ae3
上面例子里的#g5Fv;0Dvk就是我们加的盐。加完之后,密码的强度更高了,彩虹表破解的难度加大了。或者进行加盐两次 md5 :
md5(md5("123456") + "#g5Fv;0Dvk"))=f5aa59624aef811fdd8c9cb51205305f
到这里,你一定会有疑问,是不是把 md5 多做几次,或者自定义一些组合的方式就更安全了。其实不是的,黑客既然能拿到数据库里的数据,也很有可能拿到你的代码。
一个健壮的、牢不可破的系统应该是:
即使被拿走了数据和所有的代码,也没办法破解里面的数据。
这也是为什么大家不必实现自己的加密算法,而是使用公开的加密算法的原因,比如:RSA、AES、DES 等等。既然无法保证加密代码不被泄露,那就使用公开的加密算法,只要保护好私钥信息,就算你知道我的加密方式也没有任何帮助。
大部分情况下,使用 md5(md5(password) + salt) 方式加密基本上已经可以了:
md5(md5(password) + salt)
其中,最关键的是 salt 从哪里来? salt 该怎么设置才能安全。有几个重要的点:
- 不要使用固定不变的 salt。
- 每个用户的 salt 都需要不同。
- salt 要保持一定的长度。
- salt 必须由服务端使用安全的随机函数生成。
- 客户端运算需要的 salt 需要从服务端动态获取。
- 客户端加盐 hash 的结果并不是最终服务端存盘的结果。
由于客户端也需要执行加盐 hash ,所以,salt 不能直接写在客户端,而是应该动态从服务端获得。服务端生成随机的 salt 时,
必须使用安全的随机函数,防止随机数被预测。
代码实现
package com.tahacoo.exercise.salt.util;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.UUID;
public class PasswordUtils {
/**
* 实现密码加盐,加密后格式为:salt$md5(salt + password)
* @param password 明文密码
* @return 最终生成的密码
*/
public static String encrypt(String password) {
//a. 生成随机盐值
//String salt = UUID.randomUUID().toString().replace("-", "");
String salt = generateSecureSalt();
//b. 生成加盐后的密码(需要使用MD5)
String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
//c. 生成最终的密码(需要将盐值和加密后的密码拼接)
return salt + "$" + saltPassword;
}
/**
* 加盐并生成最终密码格式(方法一的重载),区别于上面的方法:这个方法是用来解密的,
* 给定了盐值,生成一个最终密码,后面要和正确的最终密码进行比对
* @param password 明文密码
* @param salt 盐值
* @return 最终生成的密码
*/
public static String encrypt(String password, String salt) {
String saltPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());
return salt + "$" + saltPassword;
}
/**
* 验证密码
* @param password 明文密码
* @param finalPassword 数据库中存放的密码
* @return
*/
public static Boolean check(String password, String finalPassword) {
//数据校验,finalPassword格式为:salt$md5(salt + password),所以最终长度为65
if (StringUtils.hasLength(password) && StringUtils.hasLength(finalPassword)
&& finalPassword.length() == 65) {
String salt = finalPassword.split("$")[0];
String checkPassword = encrypt(password, salt);
return checkPassword.equals(finalPassword);
}
return false;
}
/**
* 盐值使用UUID生成可以保证唯一性,但实际应用中建议使用加密安全的随机数生成器(SecureRandom)
* @return 32位随机数
*/
public static String generateSecureSalt() {
SecureRandom secureRandom = new SecureRandom();
//生成32位随机数
byte[] saleBytes = new byte[16];
secureRandom.nextBytes(saleBytes);
return com.mysql.cj.util.StringUtils.toHexString(saleBytes, saleBytes.length);
}
}

浙公网安备 33010602011771号