目录
一、概念
首先,哈希算法,又叫摘要算法。它的作用是对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法相同的输入一定能得到相同的输出,它的目的就是为了验证原始数据是否被篡改。
java字符串的hashCode()方法就是一个哈希算法,其输入是任意字符串,输出是固定的4字节int整数。
所有的哈希算法是单向传递的,不可逆。
二、哈希碰撞
对于相同的字符串一定可以得到相同的哈希值,但有时候不同的字符串也可以得到相同的哈希值,就比如“通话”和“重地”。
这就有问题了,那么具体我们该怎么样区分?或者说解决该问题呢?
存在哈希碰撞的情况,我们就不能局限于利用哈希编码值来区分了,前面提到过,hashCode()方法本质上不管你输入多大的数据,它的输出长度是不变的(4字节整数),所以难免有碰撞。
这就对哈希算法有要求了,一个安全的哈希算法需满足:
(1)碰撞概率低
(2)不能猜测输出
为什么说不能猜测输出呢?
是因为哈希算法是对输入内容进行加密的,是为了提高在网络传输中的信息安全性的,如果能轻易被反推出输出结果,那它的安全性是无法保证的。
三、常见哈希算法
哈希碰撞的概率要想达到尽可能的低,那么哈希算法的输出长度就要尽可能的长,长度越长重复的概率就越小,也就越安全。
算法 | 输出长度(位) | 输出长度(字节) |
MD5 | 128 bits | 18bytes |
SHA-1 | 160 bits | 20 bytes |
RipeMD-160 | 160 bits | 20 bytes |
SHA-256 | 256 bits | 32 bytes |
SHA-512 | 512 bits | 64 bytes |
首先,我们要了解一个类MessageDigest,MessageDigest为消息摘要对象,通过调用MessageDigest的getInstance(String algorithm)创建消息摘要对象。
根据传入算法名称不同,来实现不同哈希算法对象的生成。
以MD5算法为例:
//创建消息摘要对象MessageDigest
MessageDigest md5=MessageDigest.getInstance("MD5");//传入不同算法名称以便实现不同加密
MessageDigest提供了update()算法:更新原始数据
digest()算法:获取加密后结果,得到的是一个字节数组。
//更新原始数据
md5.update("天王盖地虎,宝塔镇河妖".getBytes());//输入
//获取加密后结果
//不论什么内容,加密后得到的永远是一个定长(16)数组
byte[] b=md5.digest();//输出
System.out.println("加密后结果:"+Arrays.toString(b));
我们想要看到的不仅是加密后输出的字节数组,于是我们定义了一个HashTools工具类,
//Hash算法(消息摘要算法)的工具类
public class HashTools {
//私有构造方法
private HashTools() {
}
//将字节数组转换成16进制字符串
public static String ByteToHex(byte[] bytes) {
//创建可变字符串对象
StringBuilder res=new StringBuilder();
for(byte b:bytes) {//遍历字节数组
//转换方式,按1个字节占2个字符串位转换
res.append(String.format("%02x", b));
}
return res.toString();//返回字符串
}
}
调用这个工具类的ByteToHex()方法,可以将加密后的字节数组转换成16进制的字符串。
于是,可以输出
System.out.println("加密(16进制)结果:"+HashTools.ByteToHex(b));
每次调用update()方法时,输入内容一样加密结果是一样的。
1.MD5算法
1.1 加密普通字符串完整代码
public class MD5Demo {
public static void main(String[] args) throws NoSuchAlgorithmException {
//创建消息摘要对象MessageDigest
MessageDigest md5=MessageDigest.getInstance("MD5");//传入不同算法名称以便实现不同加密
//更新原始数据
md5.update("天王盖地虎,宝塔镇河妖".getBytes());//输入
//获取加密后结果
//不论什么内容,加密后得到的永远是一个定长(16)数组
byte[] b=md5.digest();//输出
System.out.println("加密后结果:"+Arrays.toString(b));
System.out.println("加密(16进制)结果:"+HashTools.ByteToHex(b));
System.out.println("加密后长度:"+b.length);
//输入内容一样,加密结果一样
md5.update("天王盖地虎".getBytes());
md5.update(",宝塔镇河妖".getBytes());
byte[] b1=md5.digest();
System.out.println("十六进制加密结果:"+HashTools.ByteToHex(b1));
}
}
1.2 加密图片
//将图片读入字节数组
byte[] image=Files.readAllBytes(Paths.get("C:\\Users\\寻玉萌\\Pictures\\哆啦a梦.jpg"));
//创建加密对象
MessageDigest md5=MessageDigest.getInstance("MD5");
//输入图片的字节数组
md5.update(image);
byte[] md5Image=md5.digest();
System.out.println("加密后结果:"+Arrays.toString(md5Image));
//16位字节数组按一字节占两位字符串长度输出,所以16进制加密后为32位
System.out.println("16进制加密后结果:"+HashTools.ByteToHex(md5Image));
System.out.println("加密后长度:"+md5Image.length);
加密图片,唯一不同就是更新数据内容,因为update()方法传入的参数时字节数组,,将本地图片利用Files工具类的readAllBytes()方法读入更方便。
2.SHA-1算法
区别仅在创建消息摘要对象时传入SHA-1算法名称而已,这里不多赘述。
3.RipeMD-160算法
因为MessageDigest对象是通过重写getInstance()方法来达到创建不同算法对象的目的,本地的MessageDigest类中没有重写相对应的RipeMD-160的getInstance()方法,所以这里须引入第三方开源库的jar包方可使用。
在创建消息摘要对象前,需要将提供的消息摘要算法RipeMD-160注册至Security,再进行后续操作。
public class Recipe160Demo {
public static void main(String[] args) throws NoSuchAlgorithmException {
//注册BouncyCastleBouncyCastleProvider通知类
//将提供的消息摘要算法注册至Security
Security.addProvider(new BouncyCastleProvider());
//创建消息摘要对象
MessageDigest ripemd160=MessageDigest.getInstance("RipeMD160");
//更新原始数据内容
ripemd160.update("噜啦噜啦嘞绿".getBytes());
byte[] b=ripemd160.digest();//获取加密结果
System.out.println("RipeMD160算法加密后:"+HashTools.ByteToHex(b));
}
}
四、哈希算法用途
1.校验下载文件
我们下载文件时,网站上都会显示MD5哈希值
校验文件是否被篡改,只需要计算本地文件的哈希值与之对比即可判断。
2.存储用户密码
试想一下,假如数据库中存储的直接是用户的密码,那么数据库一旦泄露,那么直接将导致数据的丢失,存在严重的安全风险。
所以我们可以将直接存储用户的密码改为存储其对应的哈希值,利用对应的哈希算法计算出哈希值存入数据库,用户在进行认证时,系统根据用户输入计算哈希值,再与数据库中对应哈希值对比即可。
就像下面这样如下:
username | password |
aaa | 473a442f625606ebbe008b3379852d2fda7409fd |
bob | 2426f963654673331e8c9f5fb704c9466f8dbc98 |
这样一来,安全性得到保证。
还有值得注意的是,使用哈希口令是的彩虹表攻击。
简单来说,彩虹表就是总结出一定规律的常用密码和它们对应的MD5对照表,如果用户使用了常见的密码,黑客便可根据MD5反推出原始密码。
我们可以借助"加盐"操作来抵御彩虹表攻击。
加盐机制无非就是增加了密码的随机性,我们让加密内容由密码(password)+随机数字(salt)组成。
每次update(password)以后,再update(salt),这样每次生成的加密结果都不同,谁也无计可施。
具体实现如下:
public class MD5AndSaltDemo {
public static void main(String[] args) throws NoSuchAlgorithmException {
//密码
String password="wbjxxmy";
//盐值
String salt=UUID.randomUUID().toString().substring(0, 4);
//创建消息摘要对象
MessageDigest sha1=MessageDigest.getInstance("SHA-1");
sha1.update(password.getBytes());
sha1.update(salt.getBytes());
//获取加密结果
byte[] b=sha1.digest();
System.out.println("加密后结果为:"+Arrays.toString(b));
System.out.println("16进制加密后结果:"+HashTools.ByteToHex(b));
}
}
五、总结
1.哈希算法用于验证数据完整性,仅支持单向传递(加密),不可逆。
2.常用的哈希算法MD5、SHA-1,RipeMD-160算法。
3.用户哈希存储口令时要考虑彩虹表攻击。