目录
一.编码算法
在学习哈希算法之前我们先了解什么是编码
ASCII码就是一种编码,例如A的编码是16进制的0x41。因为ASCII码只能有127个字符:A~Z,a~z,0~9以及-,_,.,*。若相对更多文字进行编码就需要占用两个字节的Unicode 或者 三个字节的UTF-8。所以简单的编码是直接给每个字符指定一个若干字节表示的整数,复杂一点的编码就需要根据一个已有的编码推算出来,就出现了编码算法。
1.URL编码
URL 编码是浏览器发送数据给服务器时使用的编码,它通常附加在 URL 的参数 部分,例如: https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87
Java标准库提供了一个URLEncoder类用来对任意字符串进行URL编码。
我们来举个例子:
// URL编码
public class Demo01 {
public static void main(String[] args) {
// URLEncoder类:进行URL编码操作
// URLDecoder类:进行URL解码操作
// 编码处理
try {
String encoder = URLEncoder.encode("编码算法", "utf-8");
System.out.println(encoder);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 解码处理
try {
String decode = URLDecoder.decode("%E7%BC%96%E7%A0%81%E7%AE%97%E6%B3%95", "utf-8");
System.out.println(decode);
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
编码:
根据URLEncoder这个类提供的encode方法,第一个参数是想要编码的字符串,第二个参数是编码类型。
解码:
根据URLDecoder这个类提供的decode方法,同上。
2,Base64编码
URL 编码是对字符进行编码,表示成 %xx 的形式,而 Base64 编码是对二进制数据进行 编码,表示成文本格式。
Base64 编码可以把任意长度的二进制数据变为纯文本,并且纯文本内容中且只包含指定 字符内容: A ~ Z 、 a ~ z 、 0 ~ 9 、 + 、 / 、 = 。它的原理是把 3 字节的二进制 数据按 6bit 一组,用 4 个int整数表示,然后查表,把 int 整数用索引对应到字符, 得到编码后的字符串。
注:二进制按6位分一组转换成十六进制
public class Demo002 {
public static void main(String[] args) {
byte[] bytes = new byte[] {(byte)0xe4,(byte)0xb8,(byte)0xad};
//编码
String b64Encode = Base64.getEncoder().encodeToString(bytes);
System.out.println(b64Encode);
//解码
byte[] b64Decode = Base64.getDecoder().decode("5Lit");
String out = Arrays.toString(b64Decode);
System.out.println(out);
// 基于Base64解码
byte[] getDecoderRet = Base64.getDecoder().decode("5Lit");
for(byte b : getDecoderRet) {
System.out.println(Integer.toHexString(b));
}
}
}
Base64 编码的目的是把二进制数据变成文本格式,这样在很多文本中就可以处理二进制数据。例如,电 子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用 Base64 编码,然后以文本的形式传送。
Base64 编码的缺点是传输效率会降低,因为它把原始数据的长度增加了1/3。和 URL 编码一样, Base64 编码是一种编码算法,不是加密算法。
二,哈希算法
1,概述
哈希算法( Hash )又称摘要算法( Digest ),它的作用是:对任意一组输入 数据进行计算,得到一个固定长度的输出摘要。
2,特点
相同的输入一定得到相同的输出; 不同的输入大概率得到不同的输出。 所以,哈希算法的目的:为了验证原始数据是否被篡改。
3,哈希碰撞
哈希碰撞是指两个不同的输入得到两个不同的输出。这是不可避免的。
4,常见哈希算法
哈希算法输出长度越长,就越难产生碰撞,也就越安全。
算法 | 输出长度(位) | 输出长度(字节) |
---|---|---|
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算法
public class Demo01 {
public static void main(String[] args) throws NoSuchAlgorithmException {
// 创建基于MD5算法的消息摘要对象
MessageDigest md = MessageDigest.getInstance("MD5");
// 更新原始数据
md.update("天王该独户".getBytes());
md.update("马元两米五".getBytes());
// 获取加密后的结果
byte[] digestByte = md.digest();
System.out.println("加密后的结果(字节数组)"+Arrays.toString(digestByte));
System.out.println("加密后的结果(16进制字符串)"+HashTools.bytesToHex(digestByte));
System.out.println(digestByte.length);
}
}
按照MD5算法对图片进行加密:
// 按照MD5算法对图片进行加密
public class Demo02 {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
// 图片原始字节内容
byte[] imgbytes = Files.readAllBytes(Paths.get("C:\\Users\\lenovo\\Desktop\\cbx\\douban\\xuan\\123.jpg"));
//创建基于MD5的算法的消息摘要对象
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 原始字节内容
md5.update(imgbytes);
//获取加密摘要
byte[] digestByte = md5.digest();
System.out.println("加密后的结果(字节数组)"+Arrays.toString(digestByte));
System.out.println("加密后的结果(16进制字符串)"+HashTools.bytesToHex(digestByte));
System.out.println(digestByte.length);
}
}
上面两个结果可以看出加密后的字节长度固定为16。
虽然说是加密了,因为相同的输入永远会得到相同的输出,因此,如果输入被修改了,得到的输出就会不同。
有些坏蛋他根据用户常用的密码信息做成彩虹表,当然,我们也可以采取特殊措施来抵御彩虹表攻击:对每个口令额外添加随机数,这个方法称之为加盐。如下:
SHA-1算法
public class salt {
public static void main(String[] args) throws NoSuchAlgorithmException {
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");
String salt = UUID.randomUUID().toString().substring(0,4);
sha1.update("马元两米五".getBytes());
sha1.update(salt.getBytes());
byte[] dg = sha1.digest();
System.out.println(HashTools.bytesToHex(dg));
}
}
当然每次运行结果都不一样。黑客不知道我们每次加的盐也就是随机数是什么,所以也就没办法盗取密码了。
上面我们可以看出SHA-1算法与MD5算法有许多相似的地方,我们把他们封装到一个类中:
public class HashTools {
private static MessageDigest digest;
private HashTools() {}
public static String digestBymd5(String source) throws NoSuchAlgorithmException {
digest = MessageDigest.getInstance("MD5");
return handler(source);
}
public static String digestBysha1(String source) throws NoSuchAlgorithmException {
digest = MessageDigest.getInstance("SHA-1");
return handler(source);
}
public static String handler(String source) {
digest.update(source.getBytes());
byte[] bytes = digest.digest();
String hash = bytesToHex(bytes);
return hash;
}
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for(byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}
对外直接调用对应的方法即可:
public class Demo03 {
public static void main(String[] args) throws NoSuchAlgorithmException {
String md5 = HashTools.digestBymd5("马元两米五");
String sha1= HashTools.digestBysha1("马元两米五");
System.out.println(md5);
System.out.println(sha1);
}
}
三,Hmac算法
Hmac 算法就是一种基于密钥的消息认证码算法,它的全称是 Hash-based Message Au thentication Code ,是一种更安全的消息摘要算法。
HmacMD5 可以看作带有一个安全的 key 的 MD5 。使用 HmacMD5 而不是用 MD5 加 salt ,
优点:
HmacMD5 使用的 key 长度是 64 字节,更安全;
Hmac 是标准算法,同样适用于 SHA-1 等其他哈希算法;
Hmac 输出和原有的哈希算法长度一致。
// Hmac密钥
public class Demo01 {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
// 1.产生密钥
// 获取HmacMD5密钥生成器
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
// 生成密钥
SecretKey key = keyGen.generateKey();
System.out.println("密钥"+ Arrays.toString(key.getEncoded()));
System.out.println("密钥长度(64字节)"+ key.getEncoded().length);
System.out.println("密钥"+ HashTools.bytesToHex(key.getEncoded()));
// 2.使用密钥进行加密
// 获取HMac加密算法对象
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key); //初始化密钥
mac.update("马元两米五".getBytes()); // 更新原始加密内容
byte[] bytes = mac.doFinal(); // 加密处理,并获取加密结果
String result = HashTools.bytesToHex(bytes); // 加密结果处理成16进制字符串
System.out.println("加密结果16进制字符串:"+result);
System.out.println("加密结果(字节长度16字节)"+bytes.length);
System.out.println("加密结果"+result.length());
}
}
有了 Hmac 计算的哈希和 SecretKey ,我们想要验证怎么办?这时, SecretKey 不能从 KeyGenerator 生成,而是从一个 byte[] 数组恢复(通过密钥字符串或者密钥字节数组):
public class Demo02 {
public static void main(String[] args) {
String password = "马元两米五";
// 密钥字节数组
//byte[] bytes = {85, -56, -112, 81, 91, -98, -45, 35, -45, -70, -105, 32, -39, -87, 95, -35, 125, 125, 52, 24, -39, -122, -21, -113, -111, 2, 38, -65, 1, 105, 123, -85, -66, 34, 10, 41, 122, 24, 105, 98, 29, 107, -99, -106, 72, 57, 79, 22, -76, 109, 115, -86, 98, 25, 5, -56, -27, -69, 76, -70, -112, 69, 48, 127};
// 密钥(字符串)
String keyStr = "55c890515b9ed323d3ba9720d9a95fdd7d7d3418d986eb8f910226bf01697babbe220a297a1869621d6b9d9648394f16b46d73aa621905c8e5bb4cba9045307f";
byte[] keybytes = new byte[64];
for(int i = 0, k = 0;i<keyStr.length();i+=2,k++) {
String s = keyStr.substring(i,i+2);
keybytes[k] = (byte)Integer.parseInt(s, 16);
}
try {
//恢复密钥
SecretKey key = new SecretKeySpec(keybytes, "HmacMD5");
//加密
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(password.getBytes());
//加密结果
String result = HashTools.bytesToHex(mac.doFinal());
System.out.println(result);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
结果与上一次结果相同。
四,BouncyCastle
如果我们要用的算法在java标准库中没有的话,我们可以使用第三方库。
1,概述
BouncyCastle就是一个提供了很多哈希算法和加密算法的第三方开源库。它提供了 Java 标准库没有的一些算法,例如, RipeMD160哈希算法。
2,用法
添加jar包:
bcprov-jdk15on-1.70.jar
使用之前需要注册BouncyCastle(代码如下):
public class Demo01 {
public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
// 注册BouncyCastleProvider通知类
// 将提供的消息摘要算法注册至Security
Security.addProvider(new BouncyCastleProvider());
// 获取ripeMD160算法的“消息摘要对象”(加密对象)
MessageDigest ripeMD160 = MessageDigest.getInstance("RipeMD160");
// 更新原始数据
ripeMD160.update("马元两米五".getBytes());
byte[] bytes = ripeMD160.digest();
String result = HashTools.bytesToHex(bytes);
System.out.println("加密结果16进制字符串:"+result);
System.out.println("加密结果(字节长度16字节)"+bytes.length);
System.out.println("加密结果"+result.length());
}
}