目录
概念:
哈希算法又称为摘要算法,它的左右是:对于任意的一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法最显著的特点是:
- 相同的输入一定具有完全相同的输出;
- 不相同的输入大概率得到不相同的输出;
所以,哈希算法被用来验证数据是否被篡改过。
常用Hash算法:
1.MD5算法:
MD5算法输出的摘要长度为128bit,16个字节bytes;
Java的标准库中提供了常用的hash算法,通过统一的接口MessageDigest进行调用。MD5算法的代码如下:
//创建基于MD5算法的消息摘要
MessageDigest md5=MessageDigest.getInstance("MD5");
//更新原始数据
md5.update("今天是个好日子".getBytes());
//获取加密后的结果
byte[] digestBytes=md5.digest();
System.out.println("加密后的结果(字节数组):"+Arrays.toString(digestBytes));
System.out.println("加密后的结果(十六进制字符串):"+HashTools.bytesToHex(digestBytes));
System.out.println("加密后的长度:"+digestBytes.length);
在使用MessageDigest时,先通过要使用的加密算法来获取一个MessageDigest实例,然后反复调用update(byte[] bytes)方法输入数据,在所有数据输入完毕后,调用digest()方法获得加密后的摘要,为了方便观察,我们创建一个HashTools工具类来完成一些将字符数组转成十六进制字符串的操作(代码参考如下)。
//哈希算法工具类
public class HashTools {
private HashTools() {};
//将字节数组转换为16进制字符串
public static String bytesToHex(byte[] bytes) {
StringBuilder ret=new StringBuilder();
for (byte b : bytes) {
//将字节值转换为2位十六进制字符串
ret.append(String.format("%02x", b));
}
return ret.toString();
}
}
2.SHA-1算法
SHA-1算法和MD5算法使用方法基本一致,但SHA-1算法输出的摘要长度为160bit,20个字节bytes;使用代码参考如下:
//图片原始字节内容
byte[] imageBytes=Files.readAllBytes(Paths.get("C:\\text\\b.jpg"));
//创建基于SHA-1算法的消息摘要对象
MessageDigest sha1=MessageDigest.getInstance("SHA-1");
//原始字节内容
sha1.update(imageBytes);
//获取加密摘要
byte[] digestBytes=sha1.digest();
System.out.println("加密后的结果(字节数组):"+Arrays.toString(digestBytes));
System.out.println("加密后的结果(十六进制字符串):"+HashTools.bytesToHex(digestBytes));
System.out.println("加密后的长度:"+digestBytes.length);
SHA算法实际上是一个系列,包括SHA-0(已废弃)、SHA-1、SHA-256、SHA-512等。
SHA-256算法输出的摘要长度为256bit,32个字节bytes;
SHA-512算法输出的摘要长度为512bit,64个字节bytes;
所以这几种算法的使用都是一致的,为了更方便使用,我们可以直接在刚刚的HahTools类中完成重复的定义,代码参考如下:
//构造方法私有
private HashTools() {};
//消息摘要对象
private static MessageDigest digest;
//按照MD5进行消息摘要计算(哈希计算)
public static String digestByMD5(String source) throws NoSuchAlgorithmException {
digest=MessageDigest.getInstance("MD5");
return hander(source);
}
//按照SHA-1进行消息摘要计算(哈希计算)
public static String digestBySHA1(String source) throws NoSuchAlgorithmException {
digest=MessageDigest.getInstance("SHA-1");
return hander(source);
}
//按照SHA-256进行消息摘要计算(哈希计算)
public static String digestBySHA256(String source) throws NoSuchAlgorithmException {
digest=MessageDigest.getInstance("SHA-256");
return hander(source);
}
//按照SHA-512进行消息摘要计算(哈希计算)
public static String digestBySHA512(String source) throws NoSuchAlgorithmException {
digest=MessageDigest.getInstance("SHA-512");
return hander(source);
}
//通过消息摘要对象,处理加密内容
private static String hander(String source) {
digest.update(source.getBytes());
String hash=bytesToHex(digest.digest());
return hash;
}
哈希算法的一大重要用途就是用于保存用户密码,但在使用哈希加密时,往往会出现“彩虹表攻击”,即黑客为了得到用户密码,会有一个预先计算好的常用密码和它们的MD5的对照表,这个表就是彩虹表。如果用户使用了常用密码,黑客从MD5一下就能反查到原始密码。
为了避免“彩虹表攻击”,就需要进行一些特殊处理来抵御攻击:给每个密码额外提供一个随机数,这个方法称为“加盐”(详细代码参考如下):
String password="qwertyuiop";
//生产“盐”值
String salt=UUID.randomUUID().toString().substring(0, 4);
MessageDigest md5=MessageDigest.getInstance("MD5");
md5.update(password.getBytes());//原始密码
md5.update(salt.getBytes());//盐值
String digest=HashTools.bytesToHex(md5.digest());
3.Hmac算法
在前面提到的为了抵御“彩虹表攻击”,我们需要在存储用户口令时对口令进行“加盐”处理,而Hmac算法本身就是一种基于密钥的消息认证算法,它总是和某种Hash算法结合起来使用的。例如我们使用MD5算法,对应的即为HmacMD5算法,可以视为一个“加盐”的MD5算法。
因此,HmacMD5可以看作一个加了“key”的MD5算法,使用它而不是给MD5“加盐”的优点有:
- HmacMD5使用的key长度是64字节,更安全;
- Hmac是标准算法,同样适用于SHA-1等其他哈希算法;
- Hmac输出和原有的哈希算法长度一致。
Hmac的本质就是为了保证安全,我们自己不会指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key。
使用Hmac步骤:
- 通过名称HmacMD获得KeyGenerator实例
- 通过KeyGenerator创建一个SecretKey实例
- 通过HmacMD5名称获得一个Mac实例
- 调用init(SecretKey key)初始化Mac实例
- 对Mac反复调用update(byte[] bytes)更新加密内容
- 调用Mac实例的doFinal()获取最终的哈希值
详细代码参考如下:
//产生密钥
//获取密钥生成器
KeyGenerator keyGen=KeyGenerator.getInstance("HmacMD5");
//生成密钥
SecretKey key=keyGen.generateKey();
//使用密钥,进行加密
//获取Hmac加密算法对象
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);//初始化密钥
mac.update("qwertyuiop".getBytes());//更新原加密内容
String s=HashTools.bytesToHex(mac.doFinal());//加密处理并将加密结果处理成16进制字符串
System.out.println("加密结果:"+s);
System.out.println("加密结果(字符长度32位):"+s.length());
在拥有Hmac计算的哈希值和SecretKey密钥后,我们也可以进行一些操作对密钥进行恢复从而验证哈希值。
恢复密钥要通过一个字节数组来恢复
SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5");
//密钥(字节数组)
//byte[] keyBytes= {-94, 0, 34, -21, -17, 19, -52, 20, 110, -96, -12, -55, 47, 11, -41, -72, 5, -56, -50, 35, 111, 9, 93, -1, -124, -6, 65, 86, -97, -48, 51, -24, -34, -21, 96, -22, 66, -5, 38, -120, -42, -26, -110, 19, 89, -115, 28, 19, -120, 40, -78, 71, 36, -20, 23, -116, -86, -52, -64, -67, 16, -65, 35, -39};
//密钥(字符串)
String keyString="a20022ebef13cc146ea0f4c92f0bd7b805c8ce236f095dff84fa41569fd033e8deeb60ea42fb2688d6e69213598d1c138828b24724ec178caaccc0bd10bf23d9";
byte[] keyBytes=new byte[64];
for(int i=0,k=0;i<keyString.length();i+=2,k++) {
String s=keyString.substring(i,i+2);
keyBytes[k]=(byte)Integer.parseInt(s, 16);
}
//恢复密钥并进行加密
try {
//恢复密钥(字节数组)
SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5");
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);
mac.update("qwertyuiop".getBytes());
String s=HashTools.bytesToHex(mac.doFinal());
System.out.println("加密结果:"+s);
} catch (InvalidKeyException | NoSuchAlgorithmException | IllegalStateException e) {
e.printStackTrace();
}
}
4.RipeMD160算法
Java标准库中提供的哈希算法是有限的,像PipeMD160就不再Java类库中,要使用RopeMD160算法,我们需要导入BouncyCastle提供的bcprov-jdk15on-1.70.jar添加至classpath。
导入jar包后,在使用BouncyCastle提供的RipeMD160算法,需要先把BouncyCastle注册一下。
Security.addProvider(new BouncyCastleProvider());
其他的操作和其他的哈希算法基本一致,详细参考代码如下:
//注册BouncyCastleBouncyCastleProvider通知类
//将提供的消息摘要算法注册至Security
Security.addProvider(new BouncyCastleProvider());
// 创建基于RipeMD160算法的消息摘要
MessageDigest ripeMd160 = MessageDigest.getInstance("RipeMD160");
// 更新原始数据
ripeMd160.update("今天是个好日子".getBytes());
// 获取加密后的结果
byte[] digestBytes = ripeMd160.digest();
System.out.println("加密后的结果(字节数组):" + Arrays.toString(digestBytes));
System.out.println("加密后的结果(十六进制字符串):" + HashTools.bytesToHex(digestBytes));
System.out.println("加密后的长度:" + digestBytes.length);