哈希算法(Hash)又称摘要算法(Digest)。
作用:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
最重要的特点:
- 相同的输入一定得到相同的输出。
- 不同的输入大概率得到不同的输出。
哈希算法的目的:为了验证原始数据是否被篡改。
哈希算法的用途:校验下载文件、存储用户密码
哈希碰撞
哈希碰撞:两个不同的输入得到相同的输出。(eg.“通话”和“重地”)
哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
常用的哈希算法
算法 |
输出长度(位/bits) |
输出长度(字节/bytes) |
MD5 |
128 |
16 |
SHA-1 |
160 |
20 |
RipeMD-160 |
160 |
20 |
SHA-256 |
256 |
32 |
SHA-512 |
512 |
64 |
哈希算法消息摘要算法工具类
定义一个哈希算法消息摘要算法工具类 HashTools便于使用。
public class HashTools {
private static MessageDigest digest;
private void 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();
}
}
一、MD5和SHA-1算法
在Java中SHA-1和MD5是在使用上是完全一样的,只是它们的算法名称和输出长度不一样而已。
(1)对字符串进行加密
使用MessageDigest时,要先根据MD5(哈希算法)获取一个MessageDigest实例,调用update()方法跟新原始数据。调用digest()方法获得byte[]数组表示的摘要。通过调用HashTools哈希算法消息摘要算法工具类的方法,转换成16进制的字符串。
public class Demo01 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//创建基于MD5算法的消息摘要对象
MessageDigest md5=MessageDigest.getInstance("MD5");
//更新原始数据
md5.update("搏得明月出,用兰花换锦服".getBytes());
//获取加密后的结果
byte[] digestBytes=md5.digest();
System.out.println("加密后的结果(字节数组):"+Arrays.toString(digestBytes));
System.out.println("加密后的结果(16进制字符串):"+HashTools.bytesToHex(digestBytes));
System.out.println("加密结果的长度:"+digestBytes.length);
}
}
![](https://img-blog.csdnimg.cn/0ea309ca6fd5409aa7069e3e7ff76931.png)
注!!!相同的输入一定会得到相同的输出
如下图两种输入方式得到的结果是一样
//方式一
md5.update("博得明月出,用兰花换锦服".getBytes());
//方式二
md5.update("博得明月出,".getBytes());
md5.update("用兰花换锦服".getBytes());
(2)对图片进行加密
通过Files工具类的readAllBytes()方法获取到图片的原始字节内容数组。
public class Demo03 {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
//图片的原始字节内容
byte[] imageBuf=Files.readAllBytes(Paths.get("D:\\test\\20230521\\mao1.jpg"));
//创建基于算法的消息摘要对象
MessageDigest md5=MessageDigest.getInstance("MD5");
//原始字节内容
md5.update(imageBuf);
//获取加密摘要
byte[] digestBytes=md5.digest();
System.out.println("加密后的结果(字节数组):"+Arrays.toString(digestBytes));
System.out.println("加密后的结果(16进制字符串):"+HashTools.bytesToHex(digestBytes));
System.out.println("加密结果的长度:"+digestBytes.length); //MD5算法的固定输出长度为16个字节
}
}
(3)加盐
彩虹表是一个用于加密散列函数逆运算的预先计算好的表, 为破解密码的散列值(或称哈希值、微缩图、摘要、指纹、哈希密文)而准备的。
为了 抵御彩虹表的攻击,对每个口令添加额外的随机数,使得输出的结果不同。故而,即使是相同的输入内容,得到的输出结果却各不相同。
public class Demo04 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//原始密码
String password="xdsqczkyqs";
//产生随机的盐值
String salt=UUID.randomUUID().toString().substring(0,4);
//创建基于SHA-1算法的消息摘要对象
MessageDigest sha1=MessageDigest.getInstance("SHA-1");
sha1.update(password.getBytes());
sha1.update(salt.getBytes());
//计算加密结果SHA-1的输出结果为20个字节(40)
String digestHex=HashTools.bytesToHex(sha1.digest());
System.out.println(digestHex);
}
}
不加盐的输出结果:9ad8b1bf7c6f9cfb97b81725aa585cd22d8cc977
加盐后的输出结果:3429c69bd1a48a17313e9ec089193b5fcea6a313
(4)MD5和SHA-1的输出长度不同
完善工具类,由上文可知,两个算法的使用有很多代码都是一样的,避免冗余故而封装到一个工具类里。
public class HashTools {
private static MessageDigest digest;
private void HashTools() {
}
// 按照MD5进行消息摘要计算(哈希计算)
public static String digestByMD5(String source) throws NoSuchAlgorithmException {
digest = MessageDigest.getInstance("MD5");
return handler(source);
}
// 按照SHA-1进行消息摘要计算(哈希计算)
public static String digestBySHA1(String source) throws NoSuchAlgorithmException {
digest = MessageDigest.getInstance("SHA1");
return handler(source);
}
// 通过消息摘要对象处理加密内容
private static String handler(String source) {
digest.update(source.getBytes());
byte[] bytes = digest.digest();
String hash = bytesToHex(bytes);
return hash;
}
// 将字节数组转换为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();
}
}
比较两个算法的输出内容
public class Demo05 {
public static void main(String[] args) {
try {
String md5=HashTools.digestByMD5("xdsqczkyqs");
String sha1=HashTools.digestBySHA1("xdsqczkyqs");
System.out.println("md5="+md5);
System.out.println("sha1="+sha1);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
输出结果!!!两种算法的输出长度不同
md5=f27c71d02ab0444ce454a13d902f2571
sha1=9ad8b1bf7c6f9cfb97b81725aa585cd22d8cc977
二、Hmac算法
Hmac 算法就是一种基于密钥的消息认证码算法,它的全称是 Hash-based Message Authenticati on Code,是一种更安全的消息摘要算法。
Hmac 算法总是和某种哈希算法配合起来用的。例如,我们使用MD5 算法,对应的就是Hmac MD5 算法,它相当于“加盐”的MD5。HmacMD5可以看作带有一个安全的key的MD5。
使用 HmacMD5 而不是用 MD5 加 salt有如下好处:
- HmacMD5使用的 key 长度是 64 字节,更安全。
- Hmac是标准算法,同样适用于 SHA-1 等其他哈希算法。
- Hmac 输出和原有的哈希算法长度一致。
(1)对字符串通过密钥进行加密
通过KeyGenerator获取HmacMD5密钥生成器,生成随机密钥并打印。通过名称 HmacMD5获取 Mac 实例,用 Secretkey 初始化Mac实例,对 Mac实例调用 update(byte[])输入字符串的字节数组数据,调用Mac 实例的 doFinal()获取最终的哈希值。通过调用哈希算法消息摘要算法工具类的方法,获取加密后的字符串。
public class Demo06 {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
String password="xdsqczkyqs";
//获取HmacMD5密钥生成器
KeyGenerator keyGen=KeyGenerator.getInstance("HmacMD5");
//生成随机密钥
SecretKey key=keyGen.generateKey();
System.out.println("密钥:"+Arrays.toString(key.getEncoded()));
System.out.println("密钥长度:"+key.getEncoded().length);
System.out.println("密钥:"+HashTools.bytesToHex(key.getEncoded()));
//使用密钥进行加密
//获取HMac加密算法对象
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key); //初始化密钥
mac.update(password.getBytes());
byte[] bytes=mac.doFinal();
String result=HashTools.bytesToHex(bytes);
System.out.println();
System.out.println("加密结果16进制字符串:"+result);
System.out.println("加密结果(字节长度16字节):"+bytes.length);
System.out.println("加密结果(字节长度32字符):"+result.length());
}
}
![](https://img-blog.csdnimg.cn/6466878367f74f90a6fdcfc92216f1f1.png)
(2)恢复密钥
在加密是记录密钥的数据 ,以下使用数据均为上图中的数据!
通过密钥的字节数组
public class Demo07 {
public static void main(String[] args) {
//原始密码
String password="xdsqczkyqs";
//密钥(字节数组)
byte[] keyBytes= {-6, -128, 50, 95, -52, 65, -72, 32, -68, -77, -18, 104, 20, -59, -42, -39, 70, 70, 56, -96, -112, -126, 65, 37, 124, 8, 34, 71, -87, 39, -42, 106, 81, -114, -63, -92, -31, 25, -78, 104, -87, -4, -23, 91, -48, -123, 31, 82, 42, 85, 107, -5, 21, 30, 11, 105, 93, 52, 44, -68, -118, 88, 83, 28};
try {
//恢复密钥
SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5");
//创建Hmac加密算法对象
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(password.getBytes());
String result=HashTools.bytesToHex(mac.doFinal());
System.out.println(result);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
通过密钥的字符串
public class Demo07 {
public static void main(String[] args) {
//原始密码
String password="xdsqczkyqs";
//密钥(字符串)
String keyStr="fa80325fcc41b820bcb3ee6814c5d6d9464638a0908241257c082247a927d66a518ec1a4e119b268a9fce95bd0851f522a556bfb151e0b695d342cbc8a58531c";
//用于保存密钥密钥长度为64字节
byte[] keyBytes=new byte[64];
for(int i=0,k=0;i<keyStr.length();i+=2,k++) {
String s=keyStr.substring(i,i+2);
keyBytes[k]=(byte) Integer.parseInt(s, 16);
}
try {
//恢复密钥
SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5");
//创建Hmac加密算法对象
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(password.getBytes());
String result=HashTools.bytesToHex(mac.doFinal());
System.out.println(result);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
三、RipeMD160算法
输出值一般是16
进制的字符串
使用该算法时需要导入一个jar包。bcprov-jdk15on-1.70.jar
下载地址:https://mvnrepository.com/search?q=bcprov-jdk15on-1.70.jar
public class Demo08 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//注册BouncyCastleProvider通知类
//将提供的消息摘要算法注册至Security
Security.addProvider(new BouncyCastleProvider());
//获取RipeMD160算法的消息摘要对象(加密对象)
MessageDigest ripeMd160=MessageDigest.getInstance("RipeMD160");
//更新原始数据
ripeMd160.update("xdsqczkyqs".getBytes());
//获取消息摘要(加密)
byte[] result=ripeMd160.digest();
//消息摘要字节长度和内容
String hex=new BigInteger(1, result).toString(16);
System.out.println("加密结果(字符串长度):"+hex.length());
System.out.println("加密结果(字符串内容):"+hex);
}
}
加密结果(字符串长度):40
加密结果(字符串内容):71949ee603bf28cbc82b5ee91e67044f8f456026