【相关知识】
加密算法知识相关博文:浅述.Net中的Hash算法(顺带对称、非对称算法)-CSDN博客
【出处与参考】
- MessageDigest 类介绍、分多次调用update方法与一次性调用一致的说明引自:
https://blog.csdn.net/cherry_chenrui/article/details/99412886
- Java Hash算法工具类引自:
https://blog.csdn.net/c1390527393/article/details/131643488
- 非英文及利用图片进行Hash加密引自:
https://blog.csdn.net/m0_64978052/article/details/131643676
概述
哈希算法(Hash)又称摘要算法(Digest),它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法最重要的特点就是:
●相同的输入一定得到相同的输出;
●不同的输入大概率得到不同的输出。
所以,哈希算法的目的:为了验证原始数据是否被篡改。
常用哈希算法
算法 | 输出长度(位) | 输出长度(字节) |
---|---|---|
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 |
MessageDigest
java.security.MessageDigest是java提供的加密API
作用:提供MD5,SHA-1,SHA-256,SHA-512等Hash加密算法。可接受任意长度的输入,并产生固定长度的输出(输出一般可称为摘要或散列)。
通常步骤如下:
//1. 实例化一个MessageDigest对象,通过提供的静态的getInstance方法。
MessageDigest messageDigest = MessageDigest.getInstance("SHA1"); //参数值"SHA1"指的是加密的算法,大小写无所谓
//2. 输入待加密的字符串
messageDigest.update("待加密的字符串");
//3. 加密之后生成的密文的字节数组
byte[] value = messageDigest.digest();
//(一般不会直接使用生成的字节数组,而是转化成16进行字符串,长度一般可以设定)
【Tips】:MessageDigest调用digest()方法之后,MessageDigest将被重置,可以进行开始新的加密。
下面提供字节数组转化为16进制字符串的方法
/**
* 字符串数组解析成16进制字符串
* md : 待转化的字节数组
* needLen: 需要转化的16进制字符串的长度,一般都是偶数
* 说明:此算法可以设定生成的16进制字符串的长度,是拿原字节数组的前needLen/2长度的字节数组转化而来的
* 如果不需要特定长度,直接全部转,可以设置needLen的长度为md.length*2,获取去掉needLen,设定buf的长度为j*2,for循环的
* 终止条件为i<j*2 即可
* */
private static String tranform16Str(byte[] md, int needLen){
char[] hexDigits = {'0','1','2','3','4','5','6','7','8','9',
'a','b','c','d','e','f'};
try {
int j = md.length;
char buf[] = new char[needLen];
int k = 0;
for (int i = 0; i < needLen/2; i++) {
byte byte0 = md[i];
buf[k++] = hexDigits[byte0 >>> 4 & 0xf];
buf[k++] = hexDigits[byte0 & 0xf];
}
return new String(buf);
} catch (Exception e) {
log.error("加密后的密文转化为16进制字符串过程中出现异常,",e);
}
return null;
}
假如利用一个文件作为Hash加密的输入:
1. 首先先把文件读取到一个字节数组里面
File file = new File(filePath);
InputStream in = new FileInputStream(file);
byte[] allData = readInputStream(in);//获取到文件的内容2. 接下来可以有两种方式:
方式1:一段一段往里面塞
int len = allData.length;
int i = 0;
while(true){
try{
int arrLen = (len - i * 4096) > 4096 ? 4096 : (len - i * 4096);
byte[] content = new byte[arrLen];
System.arraycopy(getData, i * 4096, content, 0, arrLen);
messageDigest.update(content);
i++;
}catch (Exception e){
log.info("字节数组拷贝出现异常,表示完成 i ={}", i);
break;
}
}
byte[] transform = messageDigest.digest();
//说明,MessageDigest调用digest()方法之后 输入的摘要将被重置,意思就是之后需要再加密的话 可以直接使用之前已有的对象
String miwen = tranform16Str(transform, transform.length);方式2:一次性全部往里面塞
messageDigest.update(allData);
byte[] second = messageDigest.digest();
之后再进行16进制的转换操作。
上述两种方式的结果拿到的是一样的。 说明多次的update操作(digest方法之前)只是单纯的输入内容的追加操作。/**
* 获取输入流中的内容到字节数组里面
**/
public static byte[] readInputStream(InputStream inputStream) throws IOException {
byte[] buffer = new byte[1024];
int len = 0;
ByteArrayOutputStream bos = new ByteArrayOutputStream();
while ((len = inputStream.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.close();
return bos.toByteArray();
}
hash算法(消息摘要算法)工具类
此工具类可以有效帮助实现其他hash算法相同的功能
public class HashTools {
private static MessageDigest digest;
private HashTools(){}
//将字节数组转换为16进制字符串
public static String bytesToHex(byte[] bytes){
StringBuilder ret =new StringBuilder();
for (byte b :bytes) {
//将字节数组转换为2位16进制字符串
ret.append(String.format("%02x",b));
}
return ret.toString();
}
//按照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);
}
//通过消息摘要对象 处理加密内容
private static String handler(String source){
digest.update(source.getBytes());
byte[] bytes = digest.digest();
String hash = BytestoHex(bytes);
return hash;
}
如果只是想要通过MD5,SHA-1,SHA-256,SHA-512进行加密
直接调用此工具类中静态方法即可
//MD5算法加密后生成的字符串
System.out.println("MD5="+HashTools.digestByMD5("wbjxxmy"));
//SHA-1算法加密后生成的字符串
System.out.println("SHA-1="+HashTools.digestBySHA1("wbjxxmy"));
//SHA-256算法加密后生成的字符串
System.out.println("SHA-256="+HashTools.digestBySHA256("wbjxxmy"));
//SHA-512算法加密后生成的字符串
System.out.println("SHA-512="+HashTools.digestBySHA512("wbjxxmy"));
输出结果
但此加密依然存在风险,需注意彩虹表攻击
为了采取特殊措施来抵御彩虹表攻击:我们可以对每个口令额外添加随机数,这个方法称之为加盐(salt)
以MD5算法为例:
加盐的MD5算法
//原始密码
String passWord ="wbjxxmy";
//产生随机的盐值(以随机生成的UUID前四位为例)
String sale = UUID.randomUUID().toString().substring(0,4);
//创建基于MD5算法的消息摘要对象
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(passWord.getBytes());//原始密码
digest.update(sale.getBytes());//加盐
//生成的加密结果MD5输出结果位20个字节(40个字符)
System.out.println(Arrays.toString(digest.digest()));//20个字节
System.out.println(HashTools.bytesToHex(digest.digest()));//40长度的字符串
RipeMD160算法
Java标准库并没有提供RipeMD160算法
我们需要找一个现成的第三方库,直接使用。
BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方开源库。它提供了Java标准库没有的一些算法,例如,RipeMD160哈希算法。
首先,我们必须把BouncyCastle提供的bcprov-jdk15on-1.70.jar添加至classpath。这个jar包可以从官方网站下载
其次,Java标准库的java.security包提供了一种标准机制,允许第三方提供商无缝接入。我们要使用BouncyCastle提供的RipeMD160算法,需要先把BouncyCastle注册一下:
// 注册BouncyCastle提供的通知类对象BouncyCastleProvider
Security.addProvider(new BouncyCastleProvider());
// 获取RipeMD160算法的"消息摘要对象"(加密对象)
MessageDigest md = MessageDigest.getInstance("RipeMD160");
// 更新原始数据
md.update("wbjxxmy".getBytes());
// 获取消息摘要(加密)
byte[] result = md.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);
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。
HMac 加密
// 获取HmacMD5秘钥生成器
KeyGenerator keyGenerator =KeyGenerator.getInstance("HmacMD5");
// 产生秘钥
SecretKey key = keyGenerator.generateKey();
// 打印随机生成的秘钥:
System.out.println("字节密钥:"+Arrays.toString(key.getEncoded()));//字节输出
System.out.println("字符密钥:"+HashTools.bytesToHex(key.getEncoded()));//字符输出
// 使用HmacMD5加密
Mac mac =Mac.getInstance("HmacMD5");
// 初始化秘钥
mac.init(key);
//对Mac实例反复调用update(byte[])输入数据
mac.update("wbjxxmy".getBytes());
//调用Mac实例的doFinal()获取最终的哈希值。
byte[] bytes = mac.doFinal();
System.out.println("加密后字节:"+Arrays.toString(bytes));
System.out.println("加密后字符:"+HashTools.bytesToHex(bytes));
不过每次生成的密钥都是不同的
切记将其保存
HMac密码的校验
如果我们想要验证该密码,需通过密钥的字节数组或字符串和原始密码通过加密对比
按照“key的字节数组+原始输入”计算HMac密钥用于检验
// 原始密码
String password = "nhmyzgq";
// 通过"秘钥的字节数组",恢复秘钥
byte[] bytes ={97, -43, 1, -26, 19, 117, 107, 67, -43, -77, -70, 55, -49, 11, 115,-112, -22, 121, -28, -13, 42, -34, 21, -71, -80, 127, 33, -37, 11, 98, 45, -96, -104, -77, 46, -11, 14, 119, -115, -17, 83, -121, -98, 111, 17, -73, -18, -31, -12, 65, 5, 20, 117, 49, -79, -83, 94, 115, 67, -13, 113, 35, 102, -120};
//恢复密钥
SecretKey key = new SecretKeySpec(bytes,"HmacMD5");
// 加密
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(password.getBytes());
System.out.println("加密结果:" +HashTools.bytesToHex(mac.doFinal()));
按照“key的字符串+原始输入”计算HMac密钥用于检验
// 原始密码
String password = "nhmyzgq";
//使用字符串密钥 校验
String keyWord ="61d501e613756b43d5b3ba37cf0b7390ea79e4f32ade15b9b07f21db0b622da098b32ef50e778def53879e6f11b7eee1f44105147531b1ad5e7343f371236688";
byte[] bytes =new byte[64];
//将字符密钥以每两个字符转换位一个字节
for (int i = 0,k=0; i <keyWord.length() ; i+=2,k++) {
String s = keyWord.substring(i, i + 2);
bytes[k] = (byte)Integer.parseInt(s,16);
}
//恢复密钥
SecretKey key = new SecretKeySpec(bytes,"HmacMD5");
// 加密
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(password.getBytes());
System.out.println("加密结果:" +HashTools.bytesToHex(mac.doFinal()));
非英文信息Hash加密
通过对以上Hash算法实现的了解,可以知道Hash加密并不一定要是英文信息,可以是中文甚至是图片来进行Hash加密
1. 中文加密
//创建基于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);
//1、何事西
//2、风悲画扇
//分两次更新数据只要顺序相同获得的加密结果也是一样的
2. 图片加密
//获取图片信息
byte[] bs = Files.readAllBytes(Paths.get("D:\\3yue\\vv.jpg"));
//创建基于MD5的基本摘要信息
MessageDigest digest = MessageDigest.getInstance("MD5");
//更新数据
digest.update(bs);
//获得加密数组结果并输出
byte[] digestBytes = digest.digest();
System.out.println("加密后的结果:"+Arrays.toString(digestBytes));
//此处为一个Hashtools类写有BytestoHex
//是一个将数据转换为16进制的方法
System.out.println("加密后的结果(16进制):"+Hashtools.BytestoHex(digestBytes));
System.out.println("加密结果长度:"+digestBytes.length);