一、哈希算法概述
哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法最重要的特点就是:
1、相同的输入一定得到相同的输出;
2、不同的输入大概率得到不同的输出。
所以,哈希算法的目的:为了验证原始数据是否被篡改。
哈希算法的用途:
1、校验下载文件
我们只需要计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。
2、存储用户密码
如果直接将用户的原始口令存放在数据库中,会产生安全风险。利用哈希算法存储用户口令的哈希,在用户输入原始口令后,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,如果一致,说明口令正确,否则,口令错误。使用哈希口令时,还要注意防止彩虹表攻击,可以采用加盐(salt)的方式抵御彩虹表攻击:对每个口令额外添加随机数。
二、常用的哈希算法
哈希算法,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
常用的哈希算法有:
算法 | 输出长度(位) | 输出长度(字节) |
MD5 | 128 bits | 16 bytes |
SHA-1 | 160 bits | 20 bytes |
RipeMD-160 | 160 bits | 20 bytes |
SHA-256 | 256 bits | 32 bytes |
SHA-512 | 512 bits | 64 bytes |
2.1 MD5算法
首先创建基于MD5算法的消息摘要对象,然后调用update()输入数据,输入结束后,调用digest()方法获取byte[]数组表示的摘要,最后调用工具类中的bytesToHex()方法将字节数组转换为十六进制字符串。
//Hash算法(消息摘要算法)工具类
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();
}
}
原始内容相同,计算结果一定相同。此案例中输入的顺序相同,加密后的结果也相同,当输入顺序不同时,加密后的结果不同。 MD5算法固定输出长度为16个字节。
//MD5加密
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);
//原始内容相同,计算结果一定相同
MessageDigest Messmd5=MessageDigest.getInstance("MD5");
//更新原始数据
Messmd5.update("天王盖地虎".getBytes());
Messmd5.update("小鸡炖蘑菇".getBytes());
//获取加密后的结果
byte[] digestBytes1=Messmd5.digest();
System.out.println("加密后的结果(字节数组):"+Arrays.toString(digestBytes1));
System.out.println("加密后的结果(16进制字符串):"+HashTools.bytesToHex(digestBytes1));
System.out.println("加密后的长度:"+digestBytes1.length);
}
}
MD5算法也可以对图片进行加密,加密过程与上面对字符串的加密过程相似。
public class Demo03 {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
//图片的原始字节内容
byte[] imageBuf=Files.readAllBytes(Paths.get("D:\\test\\20230708\\bg1.jpg"));
//创建基于MD5算法的消息摘要对象
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个字节
}
}
2.2 SHA-1算法
最基础的加密过程就如上述代码展示的结果,在这个过程中,如果想要自己的加密更加安全,可以采取措施来抵御彩虹表的攻击,可以使用加盐(salt)的方法。不改变基本的加密过程步骤。
以SHA-1加盐示例:
public class Demo04 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//原始密码
String password="wbjxxmy";
//产生随机的盐值
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);
}
}
由上述代码可以看出,加盐实际上就是产生一个随机字符,来解决加密结果的不确定性和安全性问题的。
2.3 SHA-256算法和SHA-512算法
在Java中使用SHA-1、SHA-256和SHA-512,和MD5完全一样,只需要把算法名称改为对应要使用的算法名称即可。
我们可以创建一个Hash算法工具类,该工具类的作用是定义常用哈希算法进行消息摘要计算及处理加密内容的方法,在后续使用哈希算法时直接调用该类中的方法即可,避免代码重复。
//Hash算法(消息摘要算法)工具类
public class HashTools {
//消息摘要对象
private static MessageDigest digest;
//构造方法为私有
private 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("SHA-1");
return handler(source);
}
//按照SHA-256进行消息摘要计算(哈希计算)
public static String digestBySHA256(String source) throws NoSuchAlgorithmException {
digest=MessageDigest.getInstance("SHA-256");
return handler(source);
}
//按照SHA-512进行消息摘要计算(哈希计算)
public static String digestBySHA512(String source) throws NoSuchAlgorithmException {
digest=MessageDigest.getInstance("SHA-512");
return handler(source);
}
//通过消息摘要对象,处理加密内容
public 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 {
//md5加密
String md5=HashTools.digestByMD5("wbjxxmy");
//SHA-1加密
String sha1=HashTools.digestBySHA1("wbjxxmy");
//SHA-256加密
String sha256=HashTools.digestBySHA256("wbjxxmy");
//SHA-512加密
String sha512=HashTools.digestBySHA512("wbjxxmy");
System.out.println("md5="+md5);
System.out.println("sha1="+sha1);
System.out.println("sha256="+sha256);
System.out.println("sha512="+sha512);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
2.4 Hmac算法
Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。总是和某种哈希算法配合起来使用,本质上是把key密钥混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key。
使用HmacMD5为例:
先获取HmacMD密钥生成器,然后通过KeyGenerator创建SecretKey实例,生成密钥,再获取Hmac机密算法对象,调用Mac的init()方法初始化密钥,然后更新原始加密内容,最后调用Mac的doFinal()方法进行加密处理,并获取加密结果,将加密结果处理成十六进制字符串。
//Hmac算法
public class Demo01 {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
String password="wbjxxmy";
//1.产生密钥
//获取HmacMD5密钥生成器
KeyGenerator keyGen=KeyGenerator.getInstance("HmacMD5");
//生成密钥
SecretKey key=keyGen.generateKey();
System.out.println("密钥:"+Arrays.toString(key.getEncoded()));
System.out.println("密钥长度(64字节):"+key.getEncoded().length);
System.out.println("密钥:"+HashTools.bytesToHex(key.getEncoded()));
//2.使用密钥进行加密
//获取Hmac加密算法对象
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);//初始化密钥
mac.update(password.getBytes());//更新原始加密内容
byte[] bytes=mac.doFinal();//加密处理,并获取加密结果
String result=HashTools.bytesToHex(bytes);//加密结果处理成16进制字符串
System.out.println("加密结果16进制字符串:"+result);
System.out.println("加密结果(字节长度16字节):"+bytes.length);
System.out.println("加密结果(字符长度32字符):"+result.length());
}
}
使用密钥的字节数组恢复密钥。恢复密钥的语句为SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5")。
public class Demo02 {
public static void main(String[] args) {
//原始密码
String password="wbjxxmy";
//密钥(字节数组)
byte[] keyBytes= {-20, 110, 2, -67, -45, -128, 121, -30, -49, 45, -111, -88, 70, -116, 105, -25, 79, -34, -48, -13, 111, 32, -108, 70, 101, 21, 96, 98, -38, 9, 27, -80, -40, -110, 125, 125, -64, -15, 122, 112, -65, -35, 54, 91, 65, 62, -60, -4, 116, 111, -5, 25, -73, -92, 54, -121, -67, -120, 79, -48, -100, 91, 7, 17};
//恢复密钥(字节数组)
SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5");
//创建Hmac加密算法对象
try {
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);//初始化密钥
mac.update(password.getBytes());
String result=HashTools.bytesToHex(mac.doFinal());
System.out.println(result);
//9fb71485409c216aa8ca5a309747fc8b
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}
}
使用密钥的字符串恢复密钥。for循环中将密钥的字符串两位两位的取,将取到的十六进制字符转换成十进制数,并且保存在数组中,最后使用SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5")语句恢复密钥。
public class Demo03 {
public static void main(String[] args) {
//原始密码
String password="wbjxxmy";
//密钥(字符串)
String keyStr="ec6e02bdd38079e2cf2d91a8468c69e74fded0f36f20944665156062da091bb0d8927d7dc0f17a70bfdd365b413ec4fc746ffb19b7a43687bd884fd09c5b0711";
//用于保存密钥:密钥长度为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);
}
//恢复密钥(字节数组)
SecretKey key=new SecretKeySpec(keyBytes, "HmacMD5");
//创建Hmac加密算法对象
try {
Mac mac=Mac.getInstance("HmacMD5");
mac.init(key);//初始化密钥
mac.update(password.getBytes());
String result=HashTools.bytesToHex(mac.doFinal());
System.out.println(result);
//9fb71485409c216aa8ca5a309747fc8b
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}
}
2.5 RipeMD-160算法
RIPEMD-160是一种基于Merkle-Damgård结构的加密哈希函数,它是比特币标准之一。RIPEMD-160是RIPEMD算法的增强版本,RIPEMD-160算法可以产生出160位的的哈希摘要。
由于Java标准库中没有提供RIPEMD-160算法,所以我们需要导入BouncyCastle提供的bcprov-jdk15on-1.70.jar包。
首先要注册BouncyCastleProvider通知类,将提供的消息摘要算法注册至Security,该算法后续加密过程与前面的算法一致。
示例代码:
//使用第三方开源库提供的RipeMD160消息摘要算法实现
public class Demo04 {
public static void main(String[] args) throws NoSuchAlgorithmException {
//注册BouncyCastleProvider通知类
//将提供的消息摘要算法注册至Security
Security.addProvider(new BouncyCastleProvider());
//获取RipeMD160算法的"消息摘要对象"(加密对象)
MessageDigest ripeMd160=MessageDigest.getInstance("RipeMD160");
//更新原始数据
ripeMd160.update("wbjxxmy".getBytes());
//获取消息摘要(加密)
byte[] result=ripeMd160.digest();
//消息摘要的字节长度和内容
System.out.println("加密结果(字节长度):"+result.length);//160位=20字节
System.out.println("加密结果(字节内容):"+Arrays.toString(result));
//16进制内容字符串
String hex=new BigInteger(1,result).toString(16);
System.out.println("加密结果(字符串长度):"+hex.length());//20字节=40个字符
System.out.println("加密结果(字符串内容):"+hex);
}
}