概述
Hash算法又称为摘要算法,作用是:对任意一组输入数据通过计算,得到一个固定长度的输出摘要。
哈希算法的目的是为了验证原始数据被否被篡改。
哈希碰撞是指两个不同的输入得到了相同的输出,哈希碰撞不可避免,但是我们要降低哈希碰撞的概率。一个安全的哈希算法必须满足:
- 碰撞概率低
- 不能猜测输出
常见的哈希算法
哈希算法根据碰撞的概率,哈希算法的输出长度越长, 碰撞的概率越低。
常见的hash算法:
算法 | 输出长度 |
---|---|
MD5 | 16B |
SHA-1 | 20B |
SHA-256 | 32B |
SHA-512 | 64B |
RipeMD-160 | 20B |
(加密算法)MD5使用举例:(步骤)
-
创建基于相关加密算法的MessageDigest 消息概要对象
-
更新原始数据
-
获取加密后的byte数组结果
// 1.创建基于MD5加密算法的MessageDigest 消息概要对象
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 2.更新原始数据
md5.update("天王盖地虎,宝塔镇河妖!".getBytes());
// 3.获取加密后byte数组结果
byte[] bytes = md5.digest();
输出的结果:
加密后的结果[43, 116, 39, 118, 118, 24, 106, -62, 115, 122, -16, -75, -127, -71, 108, -91]
MD5摘要算法的输出长度是16B,因此,加密结果的数组长度为16
通常我们把得到的这样的字节数组转换成2位16进制的字符串保存。
即,定义一个HashTools.java工具类,并提供一个方法将得到的字节数组转换成2位16进制的字符串。
public final class HashTools{
private HashTools(){};
public satic String bytesToHex(Byte[] bytes){
StringBuilder sb = new StringBuilder();
for(byte b:bytes){
//将字节值转换为2位16进制的字符串
sb.append(String.format("%02x",b))
}
return sb.toString();
}
}
还有一种方法:
String hex = new BigInteger(1,md5.digest()).toString(16);
输出的结果一样
16进制的加密结果:2b74277676186ac2737af0b581b96ca5
2b74277676186ac2737af0b581b96ca5
使用java类库中提供的其他hash加密算法,使用的步骤一致-
哈希算法的使用
哈希算法是单向加密的算法,只能加密,过程不可逆。
用途:
校验下载的文件
计算本地下载文件的MD5哈希码与官网的MD5哈希码比较,如果一致,则说明没有被篡改。
存储用密码
使用哈希算法将加密后的密码保存至数据库,用户登录时填写的密码在进行加密然后与数据库中加密过的哈希码进行比较,
当然,这样也会存在一定的风险,黑客会将常见的密码生成彩虹表,通过MD5反查密码,也会存在一个的风险。
对此我们可以采取以下的一些策略:
-
加盐值(selt),对每个密码口令添加一些随机数、身份证位数、手机号尾数等
String password = "password"; MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); String selt = UUID.randomUUID().toString().subString(0,4); sha1.update(password); sha1.update(selt); HashTools.bytesToHex(sha1.digest());
当然这种也不是最好的,我们可以使用Hmac算法配合Hash算法实现更安全的方式
Hmac算法
Hmac算法就是一种基于密钥的消息认证码算法,Hmac算法总是和某种哈希算法配合使用的。例如,我们要使用SHA-1算法,对应的就是HmacSHA-1算法;我们要使用MD5算法,对应的就是HmacMD5算法。相当于“加盐”的SHA-1算法。
可见,Hmac的本质就是将key混入到摘要的算法中。
为了保证安全,我们不会指定一个key,而是通过java标准库中的KeyGenerator生成一个安全的随机key。
实现步骤:
- 产生秘钥key
- 获取秘钥生成器
- 生成秘钥key
- 使用秘钥进行加密
- 获取Hmac加密对象
- 初始化秘钥
- 更新原始数据
- 获取加密结果
参考代码实现:
String password = "123456"; // 获取秘钥生成器类KeyGenerator KeyGenerator keyGen = KeyGenerator.getInstance("HmacSHA1"); // 生成秘钥key SecretKey key = keyGen.generateKey(); // 对key对象进行编码为byte[] 数组 byte[] keybytes = key.getEncoded(); System.out.println(keybytes); // 获取Hmac加密对象 Mac mac = Mac.getInstance("HmacSHA1"); // 初始化秘钥 mac.init(key); //更新原始数据 mac.update(password.getBytes()); // 获取加密结果 String hex = new BigInteger(1,mac.doFinal()).toString(16);
产生的秘钥是64B
结果:
- 产生秘钥key
密钥[39, 68, -86, -5, 65, 47, 104, 75, 23, 33, -56, -63, 11, 108, 5, -109, -84, 68, 116, 83, -31, -2, 33, -118, -93, 75, -57, -125, 32, 40, -113, -24, 6, 86, 127, -59, -21, -26, 115, 58, 106, -119, -128, 22, -54, -66, -112, -82, -121, -22, 100, 33, -56, 4, -85, 96, -35, -14, 80, -101, 71, -16, -122, 4]
秘钥的长度:64
通过秘钥的字节数组恢复秘钥对象SecretKey
byte[] keybytes = {39, 68, -86, -5, 65, 47, 104, 75, 23, 33, -56, -63, 11, 108, 5, -109, -84, 68, 116, 83, -31, -2, 33, -118, -93, 75, -57, -125, 32, 40, -113, -24, 6, 86, 127, -59, -21, -26, 115, 58, 106, -119, -128, 22, -54, -66, -112, -82, -121, -22, 100, 33, -56, 4, -85, 96, -35, -14, 80, -101, 71, -16, -122, 4}
SecretKey key = new SecretKeySpec(keybytes,"HmacSHA1");
如果已经将秘钥转换成了16进制的字符串,则先将字符串转换为字节数组,再转换成秘钥对象,秘钥的字节数组是64位
String keyStr = "2744aafb412f684b1721c8c10b6c0593ac447453e1fe218aa34bc78320288fe806567fc5ebe6733a6a898016cabe90ae87ea6421c804ab60ddf2509b47f08604";
byte[] keybytes = new byte[64];
for(int i =0,k= 0;i<keyStr.length;i+=2,k++){
// 将字符串每2个分割(目前为16进制),在将其转换成十进制,并进行byte强制类型转换
keybytes[k] = (byte)Integer.parseInt(keyStr.subString(i,i+2),16);
}
SecreteKey key = new SecreteKeySpec(keybytes,"HmacShA1");
使用第三方开源提供的加密算法
eg.RipeMD160算法
步骤:
- 导包
bcprov-jdk15onjn.jar
- 注册BouncyCastleProvider通知类
Security.addProvider(new BouncyCastleProvider());
MessageDigest messageDigest = MessageDigest.getInstance("RipeMD160");
后续代码一样…
写在最后!!!
以上的代码案例,没有经过编译器编写调试,如有部分小问题,望见谅。