编码算法
首先,我们一起来学习一下编码算法,要学习编码算法首先我们就要了解一下什么是编码?
举例说明,ASCII码就是我们常见的一种编码,字母a的编码是十六进制的0x61,字母b是0x62,以此类推。
字母 | ASCII码 |
a | 0x61 |
b | 0x62 |
c | 0x63 |
... | ... |
接下来我们开始学习一下编码算法。
今天我们主要以URL编码和Base64编码为例,为大家讲解一下。
URL编码
URL 编码是浏览器发送数据给服务器时使用的编码,它通常附加在URL的参数部分,例如:https://www.baidu.com/s?wd=%E4%B8%AD%E6%96%87。
而Java标准库也提供了一个URLEncoder类来对任意字符串进行URL编码,如果服务器收到了URL编码的字符串,就可以对其进行解码,还原成原始字符串。
package com.zwd.demo01;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
public class Test01 {
public static void main(String[] args) throws UnsupportedEncodingException {
String url = "http://www.baidu.com/s?wd=";
String value = "西安";
// 对URL中的中文进行编码
String result = URLEncoder.encode(value, "utf-8");
System.out.println(result);
System.out.println(url + result);
// 对URL中的中文进行解码
String param = "https://www.baidu.com/s?wd=%E6%88%91%E6%9C%AC%E5%B0%86%E5%BF%83%E5%90%91%E6%98%8E%E6%9C%88";
String content = URLDecoder.decode(param, "utf-8");
System.out.println(content);
}
}
Base64编码
URL编码是对字符进行编码,表示成%xx的形式,而Base64编码编码是对二进制数据进行编码,表示成文本格式。
首先,我们可以读取图片。
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class Test02 {
public static void main(String[] args) throws IOException {
// 读取图片(字节数组)
byte[] imageByteArray = Files.readAllBytes(Paths.get("D:\\net\\1.jpg"));
// 将字节数组进行Base64编码,转换成"字符串形式"
String imageDataStr = Base64.getEncoder().encodeToString(imageByteArray);
// Base64解码
byte[] imageResultByteArray = Base64.getDecoder().decode(imageDataStr);
Files.write(Paths.get("D:\\http\\coder\\帅哥.jpg"), imageResultByteArray);
}
}
也可以从文本文件中读取一张图片的Base64编码值
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
public class Test03 {
public static void main(String[] args) throws IOException {
// 从文本文件中读取一张图片的Base64编码值
List<String> lines = Files.readAllLines(Paths.get("D:\\net\\周杰伦.txt"));
StringBuilder sb = new StringBuilder();
for(String ln : lines) {
sb.append(ln);
}
System.out.println(sb.length());
// Base64解码
byte[] imageByteArray = Base64.getDecoder().decode(sb.toString());
Files.write(Paths.get("D:\\http\\coder\\jay.jpg"), imageByteArray);
}
}
还有从文本文件中读取一首mp3的Base64编码值
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.List;
public class Test04 {
public static void main(String[] args) throws IOException {
// 从文本文件中读取一首mp3的Base64编码值
List<String> lines = Files.readAllLines(Paths.get("D:\\http\\coder\\mojito.txt"));
StringBuilder sb = new StringBuilder();
for(String ln : lines) {
sb.append(ln);
}
// Base64解码
byte[] mp3ByteArray = Base64.getDecoder().decode(sb.toString());
Files.write(Paths.get("D:\\music\\mojito.mp3"), mp3ByteArray);
}
}
哈希算法
哈希算法( Hash )又称摘要算法( Digest)。它的作用是:对任意一组输入数据进行计算,得到一个固定长度的输出摘要。
哈希算法的目的:推为了验证原始数据是否被篡改。
哈希算法最重要的特点就是:相同的输入-定得到相同的输出;不同的输入大概率得到不同的输出。
还有一个概念,我们需要了解,就是哈希碰撞。
哈希碰撞就是指:两个不同的输入,得到了相同的输出,举例说明:
"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0
"通话".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03
有的人就会问了:哈希碰撞能不能避免?答案是:不可以。因为输出的字节长度是固定的,String的hasjCode()输出的是4字节的整数,它最多只有4294967296种输出结果,但是输入的数据长度是不固定的,有无数种的输入可能,所以它必然会产生哈希碰撞。
哈希算法的安全性也是由哈希碰撞的概率高低决定的,一个安全的哈希算法必须满足:碰撞概率低;不能猜测输出。
不能猜测输出是指:输入的任意一个bit的变化会造成输出结果完全不同,这样就很难从输出反推输入。
常用的哈希算法
算法 | 输出长度(位) | 输出长度(字节) |
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算法
我们首先以MD5为例,一起来看如何对输入计算哈希:
首先,我们需要创建一个MessageDigest对象,这是为了获取基于MD5加密算法的工具对象。
然后,使用updata()方法,对原始数据进行更新。
接着,创建一个字节数组用于接收加密后的数据。因为digest()方法的返回值是一个字节数组。
最后,我们可以得到加密后的数据,把它转换成十六进制的字符串。
我们可以看到,只要输入的内容一致,不管是怎样输入,输出结果都是一样的。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Test01 {
public static void main(String[] args) {
try {
// 获取基于MD5加密算法的工具对象
MessageDigest md5 = MessageDigest.getInstance("MD5");
// 更新原始数据
md5.update("hello".getBytes());
md5.update("world".getBytes());
// 获取加密后的结果
byte[] resultByteArray = md5.digest();
System.out.println(Arrays.toString(resultByteArray));
// 只要内容相同,加密的结果相同
MessageDigest tempMd5 = MessageDigest.getInstance("MD5");
tempMd5.update("helloworld".getBytes());
byte[] tempResultByteArray = tempMd5.digest();
System.out.println(Arrays.toString(tempResultByteArray));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
输出结果:
我们也可以将最后得到的加密数据使用StirngBulider将其拼接起来。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class Test02 {
public static void main(String[] args) {
String password = "wbjxxmynhmyzgq";
try {
// 根据当前算法,获取加密工具对象(摘要)
MessageDigest digest = MessageDigest.getInstance("MD5");
// 更新原始数据
digest.update(password.getBytes());
// 加密后的字节数组,转换字符串
byte[] resultByteArray = digest.digest();
StringBuilder result = new StringBuilder();
for(byte bite : resultByteArray) {
result.append(String.format("%02x", bite));
}
System.out.println(result);
System.out.println(result.length());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
输出结果:
在 使用哈希口令时,还要注意防止彩虹表攻击。
什么是彩虹表呢?上面讲到了,如果只拿到MD5,从MD5反推明文口令,只能使用暴力穷举的方法。然而黑客并不笨,暴力穷举会消耗大量的算力和时间。但是,如果有一个预先计算好的常用口令和它们的MD5的对照表,这个表就是彩虹表。如果用户使用了常用口令,黑客从MD5一下就能反查到原始口令。
对于这种情况,我们可以选择对MD5进行加盐(salt),来抵御彩虹表攻击。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.UUID;
public class Test04 {
public static void main(String[] args) {
String password = "wbjxxmynhmyzgq";
String salt = UUID.randomUUID().toString().substring(0, 5);
System.out.println(salt);
try {
// 获取MD5算法的工具对象
MessageDigest digest = MessageDigest.getInstance("MD5");
digest.update(password.getBytes());
digest.update(salt.getBytes());
byte[] reaultByteArray = digest.digest();
System.out.println(Arrays.toString(reaultByteArray));
System.out.println(reaultByteArray);
StringBuilder result = new StringBuilder();
for(byte b : reaultByteArray) {
result.append(String.format("%02x", b));
}
System.out.println(result);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
SHA-1算法
在Java中,SHA-1的使用方法和MD5完全一样,只需要把算法的名称改为"SHA-1"即可。
public class demo {
public static void main(String[] args) {
// 创建一个MessageDigest实例:
MessageDigest md = MessageDigest.getInstance("SHA-1");
// 反复调用update输入数据:
md.update("Hello".getBytes("UTF-8"));
md.update("World".getBytes("UTF-8"));
// 20 bytes: db8ac1c259eb89d4a131b253bacfca5f319d54f2
byte[] results = md.digest();
StringBuilder sb = new StringBuilder();
for(byte bite : results) {
sb.append(String.format("%02x", bite));
}
System.out.println(sb.toString());
}
}
Hamc算法
接着,我们再了解一下Hamc算法。我们先回顾一下上面提到的哈希算法:(digest = hash(input)),为了抵御彩虹表攻击,我们会选择加盐,其目的就是为了能让输出有所变化:(digest = hash(salt + input))。
而Hamc算法就是一种基于密钥的消息认证码算法,它的全称是:Hash-based Message Authentication Code,是一种更安全的消息摘要算法。
Hmac算法总是以某种算法配合起来使用的,我们就以MD5为例。这样就可以看成HmacMD5,它可以看作带有一个安全的key的MD5。相比较于加salt的MD5它的好处有:
·HmacMD5使用的key长度是64字节,更加安全。
·Hmac是标准算法,同样适用于SHA-1等其他哈希算法。
·Hmac输出和原有的哈希算法长度一致。
第一:通过名称HmacMD5获取KeyGenerator实例。
第二:通过KeyGenerator创建一个SecretKey实例。
第三:通过名称HmacMD5获取Mac实例。
第四:用SecretKey初始化Mac实例。
第五:对Mac实例反复调用updata()方法输入数据。(此方法需传入字节数组)
第六:调用Mac实例的doFinal()获取最终的哈希值。
以下提供HmacMD5的参考代码:
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
public class Test06 {
public static void main(String[] args) {
String password = "wbjxxmynrmyzgq";
try {
// 1. 生成密钥
// 密钥生成器KeyGenerator
KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");
// 生成密钥
SecretKey key = keyGen.generateKey();
// 获取密钥key的字节数组(64)
byte[] keyByteArray = key.getEncoded();
System.out.println("密钥长度:" + keyByteArray.length + "字节");
StringBuilder keyStr = new StringBuilder();
for(byte b : keyByteArray) {
keyStr.append(String.format("%02x", b));
}
System.out.println("密钥内容:" + keyStr);
System.out.println("密钥内容的长度:" + keyStr.length());
// 2.使用密钥,进行加密
// 获取算法对象
Mac mac = Mac.getInstance("HmacMD5");
// 初始化密钥
mac.init(key);
// 更新加密内容
mac.update(password.getBytes());
// 加密
byte[] resultByteArray = mac.doFinal();
System.out.println("加密结果:" + resultByteArray.length + "字节");
StringBuilder resultStr = new StringBuilder();
for(byte b : resultByteArray) {
resultStr.append(String.format("%02x", b));
}
System.out.println("加密结果:" + resultStr);
System.out.println("加密结果的长度:" + resultStr.length());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}
}
通过Hmac计算了哈希和SecretKey,如果我们想要验证该怎么办呢?
这时SecretKey不能从KeyGenerator生成了,而是从一个byte[]数组恢复:
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class Test07 {
public static void main(String[] args) {
// 原始密码
String password = "nhmyzgq";
try {
// 通过"密钥的字节数组",恢复密钥
byte[] keyByteArray = {126, 49, 110, 126, -79, -5, 66, 34, -122, 123, 107, -63, 106, 100, -28, 67, 19, 23, 1, 23, 47, 63, 47, 109, 123, -111, -27, -121, 103, -11, 106, -26, 110, -27, 107, 40, 19, -8, 57, 20, -46, -98, -82, 102, -104, 96, 87, -16, 93, -107, 25, -56, -113, 12, -49, 96, 6, -78, -31, -17, 100, 19, -61, -58};
SecretKey key = new SecretKeySpec(keyByteArray, "HmacMD5");
// 加密
Mac mac = Mac.getInstance("HmacMD5");
mac.init(key);
mac.update(password.getBytes());
byte[] resultByteArray = mac.doFinal();
StringBuilder resultStr = new StringBuilder();
for(byte b : resultByteArray) {
resultStr.append(String.format("%02x", b));
}
System.out.println(resultStr);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
恢复SecretKey的语句就是new SecretKeySpec(keyByteArray, "HmacMD5");
RipeMD160算法
最后,我们再浅浅的说一下RipeMD160算法。RipeMD160算法是一种基于Merkel-Damgard结构的加密哈希函数。 RipeMD160算法在java标准库中并没有提供,所以我们需要使用BouncyCastle提供的第三方开源jar包手动导入,我们可以从官方网站下载jar包。
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.Arrays;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class Main01 {
public static void main(String[] args) {
try {
// 注册BouncyCastle提供的通知类对象BouncyCastleProvider
Security.addProvider(new BouncyCastleProvider());
// 获取RipeMD160算法的"消息摘要类"(加密对象)
MessageDigest digest = MessageDigest.getInstance("RipeMD160");
// 更新原始数据
digest.update("helloWorld".getBytes());
// 获取消息摘要(加密)
byte[] result = digest.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);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}