JAVASE中的哈希算法简单实现

JAVASE中的哈希算法简单实现哈希算法概述 哈希算法 Hash 又称摘要算法 Digest 作用是 对任意一组输入数据进行计算 得到一个固定长度的输出摘要 哈希算法最重要的特点就是 相同的输入一定 得到相同的输出 不同的输入大概率 得到不同的输出 So 哈希算法的目的 为了验证原始数据是否被篡改

大家好,我是讯享网,很高兴认识大家。

哈希算法概述:

哈希算法(Hash):

又称摘要算法(Digest),作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法最重要的特点就是:
相同的输入一定得到相同的输出
不同的输入大概率得到不同的输出

So,哈希算法的目的:为了验证原始数据是否被篡改

为何不同的输入得到的输出只是大概率不同,这便是接下来要讲述的哈希碰撞

哈希碰撞:

哈希碰撞是指:两个不同的输入得到了相同的输出

碰撞能不能避免?答案是不能。碰撞是一定会出现的,因为输出的字节长度是固定的,Java中String的hashCode()就是一个哈希算法,它的输入是任意字符串,输出是固定4字节整数,最多只有种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。
碰撞不可怕,我们要担心的不是碰撞,而是碰撞的概率,因为碰撞概率的高低关系到哈希算法的安全性。一个安全的哈希算法必须满足:
碰撞概率低
不能猜测输出
不能猜测输出是指:输入的任意一个bit的变化会造成输出完全不同,这样就很难从输出反推输入(只能依靠暴力穷举)

常用哈希算法:

算法

输出长度(位)

输出长度(字节)

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

接下来,我们用代码实现常见的两种哈希算法:MD5与SHA-1


讯享网

MD5:

Java标准库提供了常用的哈希算法,通过统一的接口进行调用。以MD5算法为例,看看如何对输入内容计算哈希

Ⅰ.加密文本信息

public class md5 { public static void main(String[] args) throws NoSuchAlgorithmException { //创建基于MD5算法的消息摘要对象 MessageDigest md5 = MessageDigest.getInstance("md5"); //更新原始数据 md5.update("我本将心向明月".getBytes()); //获取加密后的结果 byte[] disest = md5.digest(); System.out.println(Arrays.toString(disest));//加密后的结果 System.out.println(HashTools.bytesToHex(disest));//将字节数组转换为16进制字符串 System.out.println(disest.length);//加密结果的长度 } } //Hash算法(信息摘要算法)工具 public class HashTools{ private HashTools(){} public static String bytesToHex(byte[] bytes){ StringBuilder ret = new StringBuilder(); //将字节数组转换为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(); } } }

讯享网

使用MessageDigest时,我们首先根据哈希算法获取一个MessageDigest实例,然后,反复调用update(byte[])输入数据。当输入结束后,调用digest()方法获得byte[]数组表示的摘要,最后,把它转换为十六进制的字符串

Ⅱ.加密图片信息

讯享网//按照MD5算法对图片进行“加密” public class ImageHash { public static void main(String[] args) throws IOException, NoSuchAlgorithmException { //图片原始字节内容 byte[] images = Files.readAllBytes(Paths.get("d:\\text\\java.png")); //创建基于MD5算法的消息摘要对象 MessageDigest md5 = MessageDigest.getInstance("md5"); //原始字节内容(图片) md5.update(images); //获取加密摘要 byte[] disest = md5.digest(); System.out.println(Arrays.toString(disest)); System.out.println(HashTools.bytesToHex(disest)); System.out.println(disest.length);//MD5算法固定输出长度为16个字节 } } //Hash算法(信息摘要算法)工具 public class HashTools{ private HashTools(){} public static String bytesToHex(byte[] bytes){ StringBuilder ret = new StringBuilder(); //将字节数组转换为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(); } }

对比两个实现方法不难发现,Hash算法工具代码块一直重复出现,所有我们可以将这部分代码封装在HashTools方法中

//Hash算法工具类 public class HashTools { private HashTools(){}//构造方法私有 private static MessageDigest disgest;//消息摘要对象 //按照MD5进行消息摘要计算 public static String disgestByMD5(String source) throws NoSuchAlgorithmException { disgest = MessageDigest.getInstance("MD5"); return handler(source); } //按照SHA1进行消息摘要计算 public static String disgestBySHA1(String source) throws NoSuchAlgorithmException { disgest = MessageDigest.getInstance("SHA-1"); return handler(source); } //通过信息摘要对象,处理加密内容 private static String handler(String source){ disgest.update(source.getBytes());//调用update输入数据 byte[] bytes = disgest.digest();//调用digest()方法获得由byte[]数组表示的摘要 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(); } } 

SHA-1:

MD5与SHA-1算法原理相同,区别仅在于输出长度与字节大小的差异,我们可直接将SHA-1的实现方法封装在HashTools方法中(在Java中使用SHA-1,和MD5完全一样,只需要把算法名称改为"SHA-1")

讯享网public class hashdemo { public static void main(String[] args) throws NoSuchAlgorithmException { //创建基于MD5算法的消息摘要对象 //MessageDigest md5 = MessageDigest.getInstance("md5"); //创建基于SHA-1算法的消息摘要对象 //MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); //通过调用HashTools实现 String md5 = HashTools.disgestByMD5("落花有意"); String sha1 = HashTools.disgestBySHA1("流水无情"); System.out.println(md5); System.out.println(sha1); } } 

哈希算法用途:

校验下载文件:

因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。如何判断下载到本地的软件是原始的、未经篡改的文件?我们只需要自己计算一下本地文件的哈希值,再与官网公开的哈希值对比,如果相同,说明文件下载正确,否则,说明文件已被篡改。

存储用户密码:

哈希算法的另一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:
数据库管理员能够看到用户明文口令
数据库数据一旦泄漏,黑客即可获取用户明文口令

不存储用户的原始口令,那么如何对用户进行认证?方法是存储用户口令的哈希,例如,MD5。在用户输入原始口令后,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,如果一致,说明口令正确,否则,口令错误。

这样一来,数据库管理员看不到用户的原始口令。即使数据库泄漏,黑客也无法拿到用户的原始口令。想要拿到用户的原始口令,必须用暴力穷举的方法,一个口令一个口令地试,直到某个口令计算的MD5恰好等于指定值。
使用哈希口令时,还要注意防止彩虹表攻击
什么是彩虹表?上面讲到,如果只拿到MD5,从MD5反推明文口令,只能使用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令(这就是为什么不要使用常用密码,以及不要使用生日作为密码的原因)

当然,我们也可以采取特殊措施来抵御彩虹表攻击:对每个口令额外添加随机数,这个方法称之为加盐(salt): digest = md5(salt + inputPassword)

代码实现:

public class HashPassWord { public static void main(String[] args) throws NoSuchAlgorithmException { //原始密码 String password = "我本将心向明月"; //创建基于MD5算法的消息摘要对象 MessageDigest md5 = MessageDigest.getInstance("md5"); //产生随机盐值 String salt = UUID.randomUUID().toString().substring(0,4); md5.update(password.getBytes());//原始密码 md5.update(salt.getBytes());//盐值 String disgest = HashTools.bytesToHex(md5.digest());//计算加密结果 System.out.println(disgest); } } 

But,自己手动加"盐"还是过于麻烦且安全指数直线下降,所以在这里我们需要来了解一种更为安全的算法:Hmac

Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。
Hmac算法总是和某种哈希算法配合起来用的。例如,我们使用MD5算法,对应的就是Hmac MD5算法,它相当于“加盐”的MD5:HmacMD5 ≈ md5(secure_random_key, input)
因此,HmacMD5可以看作带有一个安全的key的MD5。使用HmacMD5而不是用MD5加salt,有如下好处:
HmacMD5使用的key长度是64字节,更安全
Hmac是标准算法,同样适用于SHA-1等其他哈希算法
Hmac输出和原有的哈希算法长度一致
可见,Hmac本质上就是把key混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key。为了保证安全,我们不会自己指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key
下面是使用HmacMD5的参考代码:

讯享网public class hmac { public static void main(String[] args) { String index = "我本将心向明月"; try { //获取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(index .getBytes());//更新原始加密内容 byte[] bytes = mac.doFinal();//加密处理,获取加密结果 String result = HashTools.bytesToHex(bytes);//加密结果处理为16进制字符串 System.out.println(result); System.out.println(bytes.length); System.out.println(result.length()); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } } }

和MD5相比,使用HmacMD5的步骤是:
1通过名称HmacMD5获取KeyGenerator实例
2通过KeyGenerator创建一个SecretKey实例
3通过名称HmacMD5获取Mac实例
4用SecretKey初始化Mac实例
5对Mac实例反复调用update(byte[])输入数据
6调用Mac实例的doFinal()获取最终的哈希值

有了Hmac计算的哈希和SecretKey,我们想要验证怎么办?这时,SecretKey不能从KeyGenerator生成,而是从一个byte[]数组恢复:

public class DisHmac { public static void main(String[] args) { //原始密码 String password = "我本将心向明月"; //按照字节数组恢复Hmac密钥 //字节密钥 byte[] key = {-93, -65, -45, 40, 119, -32, -118, 83, 90, -122, -10, 54, -8, 84, -48, 75, 21, -93, 58, 28, 124, 107, -89, -64, 18, -4, 50, -18, -27, -118, -45, 114, 21, 45, 112, 123, 124, -85, -109, 93, 74, 99, -12, -36, -85, -93, -20, -12, -2, 33, 123, -53, -69, -66, 53, -76, -65, 54, -62, -77, 111, 30, -86, 80}; //持有字符密钥时恢复密钥 //String key = "a3bfd32877e08a535a86f636f854d04b15a33a1c7c6ba7c012fc32eee58add707b7cab935d4a63f4dcaba3ecf4fe217bcbbbbe35b4bf36c2b36f1eaa50"; //保存密钥,长度为64字节 //byte[] keyword = new byte[64]; //for (int i = 0,k = 0;i<key.length();i+=2,k++){ //String s = key.substring(i,i+2); //keyword[k] = (byte)Integer.parseInt(s,16);//转化为16进制byte值 //} //System.out.println(Arrays.toString(keyword));//可观察到将密钥转化为字节数组 try { //恢复密钥 SecretKey secretKey = new SecretKeySpec(key,"HmacMD5"); //创建Hmac加密算法对象 Mac mac = Mac.getInstance("HmacMD5"); mac.init(secretKey);//初始化密钥 mac.update(password.getBytes()); String result = HashTools.bytesToHex(mac.doFinal()); //f5aa6840c061ffac15e69f2b0ee3b59c System.out.println(result); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } } } 

Java标准库提供了一系列常用的哈希算法。但如果我们要用的某种算法,Java标准库没有提供时,抛开自己写一个这种不实际的想法,我们还可以找一个第三方库,BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方开源库。它提供了Java标准库没有的一些算法,例如,RipeMD160哈希算法:

讯享网//使用第三方开源库提供的RipeMD160信息摘要算法实现 public class RipeMD160 { public static void main(String[] args) throws NoSuchAlgorithmException { //注册BouncyCastleBouncyCastleProvider通知类 //将提供的消息类摘要算法注册至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); } }

小结:

1.哈希算法可用于验证数据完整性,具有防篡改检测的功能
2.常用的哈希算法有MD5、SHA-1等
3.用哈希存储口令时要考虑彩虹表攻击

4.BouncyCastle是一个开源的第三方算法提供商,提供了很多Java标准库没有提供的哈希算法和加密算法

5.Hmac算法是一种标准的基于密钥的哈希算法,可以配合MD5、SHA-1等哈希算法,计算的摘要长度和原摘要算法长度相同。

                                                                                          向上攀爬的痛苦,终会在登顶时烟消云散

                                       ——ZQY

小讯
上一篇 2025-02-07 13:22
下一篇 2025-03-17 14:06

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/44881.html