概述
定义
哈希算法(Hash)又称为摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
特点
- 哈希算法是不可逆的,不能反向推出明文。
- 相同的输入一定会得到相同的输出。
- 不同的输入大概率会得到不同的输出。
应用场景
- 检测原始数据 软件、文件是否篡改;
- 伪随机数生成;一次性口令;
- 密码存储;
Java字符串中的hashCode()就是一个哈希算法,它的输入是任意字符串,输出是固定的4字节int整数。
两个相同字符串永远会计算出相同的hashCode,否则基于hashCode定位的HashMap就无法正常工作。这也是为什么当我们自定义一个class时,重写equals()方法时我们必须正确重写hashCode()方法。
重写equals()方法为什么必须重写hashCode()方法?
equals方法用于判断两个对象是否相等,而hashCode方法用于计算对象的哈希值。在使用集合类(如HashSet、HashMap等)时,它们内部会用到hashCode方法来快速定位元素,然后再用equals方法来比较元素是否相等。
只重写了equals方法而没有重写hashCode方法,它将使用默认的hashCode实现。这将导致两个逻辑上相等的对象计算出的哈希值不同,从而导致集合类无法正确地操作这些对象。
为了保证集合类能够正常运作,必须重写hashCode方法,以保证逻辑上相等的对象计算出的哈希值相同。
哈希碰撞
输入两个不同的值经过hash计算后,得到的hash值相同。
例如:
"AaAaAa".hashCode();//0x7460e8c0
"BBAaBB".hashCode();//0x7460e8c0
"通话".hashCode();//0x11ff03
"重地".hashCode();//0x11ff03
哈希碰撞是不可避免的,String的hashCode()把无限的输入集合映射到一个有限的输出集合,必然会产生碰撞。
常用哈希算法
哈希算法,根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
算法 | 输出长度(位) | 输出长度(字节) |
---|---|---|
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 |
Java标准库提供了常用的哈希算法,通过统一的接口进行调用。以MD5算法为例,看看如何计算哈希: |
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Demo05 {
public static void main(String[] args) {
String s = "asdfasdf";
try {
//创建消息摘要算法对象(MD5算法)
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
//添加原文内容(允许添加多次)
messageDigest.update(s.getBytes());
//消息摘要计算(执行哈希算法)
byte[] data = messageDigest.digest();
StringBuilder sb = new StringBuilder();
for(byte b : data) {
sb.append(String.format("%02x", b));
}
System.out.println("加密结果:"+ Arrays.toString(data));
System.out.println("加密长度:"+data.length);
System.out.println("密文字符:"+sb);
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
使用MessageDigest时,我们首先根据哈希算法获取一个MessageDigest实例,然后,反复调用update(byte[])输入数据。调用digest()方法获得byte[]数组表示的摘要,最后,把它转换为十六进制的字符串。
使用哈希算法存储用户密码
哈希算法的一个重要用途是存储用户口令。如果直接将用户的原始口令存放到数据库中,会产生极大的安全风险:
- 数据库管理员能看到用户明文口令。
- 数据库数据一旦泄露,黑客即可获取明文口令。
使用MD5对用户口令进行加密,在用户输入原始口令后,系统计算用户输入的原始口令的MD5并与数据库存储的MD5对比,一致则正确。
使用哈希口令时,会被彩虹表攻击,彩虹表是指一个预先计算好的常用口令和它们的MD5的对照表,这个表就是彩虹表。
我们可以采取特殊措施来地狱彩虹表攻击:对每个口令额外添加随机数,这个方法称之为加盐(salt):
digest = md5(salt + inputPassword)
HmacMD5算法
HmacMD5 ≈ md5(secure_random_key, input)
Hmac算法就是一种基于密钥的消息认证码算法,它的全称是Hash-based Message Authentication Code,是一种更安全的消息摘要算法。
因此,HmacMD5可以看作带有一个安全的key的MD5。使用HmacMD5而不是用MD5加salt,有如下好处:
- HmacMD5使用的key长度是64字节,更安全;
- Hmac是标准算法,同样适用于SHA-1等其他哈希算法;
- Hmac输出和原有的哈希算法长度一致。
可见,Hmac本质上就是把key混入摘要的算法。验证此哈希时,除了原始的输入数据,还要提供key。为了保证安全,我们不会自己指定key,而是通过Java标准库的KeyGenerator生成一个安全的随机的key。
public class Demo10 {
public static void main(String[] args) {
String password="123456789";
try {
//创建密钥生成器对象
KeyGenerator keygen = KeyGenerator.getInstance("HmacMD5");
//生成密钥
SecretKey secretKey = keygen.generateKey();
//获取秘钥原始内容(字节数组)
byte[] keyArray = secretKey.getEncoded();
//Base64
String keyStr = Base64.getEncoder().encodeToString(keyArray);
//基于密钥,进行MD5的消息摘要计算
Mac mac = Mac.getInstance("HmacMD5");
//初始化密钥(加入密钥)
mac.init(secretKey);
//添加密钥内容
mac.update(password.getBytes());
//最终计算
byte[] ret = mac.doFinal();
StringBuilder sb = new StringBuilder();
for (byte b : ret) {
sb.append(String.format("%02x", b));
}
System.out.println(keyArray.length);
System.out.println(keyStr);
System.out.println(ret.length);
System.out.println(sb);
} catch (Exception e) {
// TODO: handle exception
}
}
}
和MD5相比,使用HmacMD5的步骤是:
- 通过名称HmacMD5获取KeyGenerator实例;
- 通过KeyGenerator创建一个SecretKey实例;
- 通过名称HmacMD5获取Mac实例;
- 用SecretKey初始化Mac实例;
- 对Mac实例反复调用update(byte[])输入数据;
- 调用Mac实例的doFinal()获取最终的哈希值。
在验证是否相同时,需要保存密钥的内容并使用SecretKey恢复。
public class Demo11 {
public static void main(String[] args) {
try {
String keyStr ="S/cSmL2sMMRZZhxkMsdqk9hT/LzELj0sz74iQ4VqW4wIcCvPqTe/BnfcB229tI47+7sZNuCCnk3EHPXlf8z/5A==";
String input = "123456789";
//恢复秘钥
byte[] keyArray = Base64.getDecoder().decode(keyStr);
SecretKey key = new SecretKeySpec(keyArray, "HmacMd5");
//计算
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(input.getBytes());
byte[] retArray = mac.doFinal();
StringBuilder ret = new StringBuilder();
for (byte b : retArray) {
ret.append(String.format("%02x", b));
}
System.out.println(keyArray.length);
System.out.println(keyStr);
System.out.println(retArray.length);
System.out.println(ret);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}