目录
URL编码
之所以需要 URL 编码,是因为出于兼容性考虑,很多服务器只识别 ASCII 字符。但如果 UR L 中包含中文、日文这些非 ASCII 字符怎么办?不要紧, URL 编码有一套规则: 如果字符是 A ~ Z , a ~ z , 0 ~ 9 以及 - 、 _ 、 . 、 * ,则保持不变; 如果是其他字符,先转换为 UTF-8 编码,然后对每个字节以 %XX 表示。
要特别注意: URL 编码是编码算法,不是加密算法。 URL 编码的目的是把任意文本数据编码为 % 前缀表示的文本,编码后的文本仅包含 A ~ Z , a ~ z , 0 ~ 9 , - , _ , . , * 和 % , 便于浏览器和服务器处理。
基于URL编码的解码实例:
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 编码是对二进制数据进行编 码,表示成文本格式。
Base64 编码可以把任意长度的二进制数据变为纯文本,并且纯文本内容中且只包含指定字符 内容: A ~ Z 、 a ~ z 、 0 ~ 9 、 + 、 / 、 = 。它的原理是把 3 字节的二进制数据按 6bi t 一组,用 4 个int整数表示,然后查表,把 int 整数用索引对应到字符,得到编码后的字符串。
6 位整数的范围总是 0 ~ 63 ,所以,能用 64 个字符表示:字符 A ~ Z 对应索引 0 ~ 25 ,字符 a ~ z 对应索引 26 ~ 51 ,字符 0 ~ 9 对应索引 52 ~ 61 ,最后两个索引 62 、 63 分别用字符 + 和 / 表示。
基于Base64编码的实例(读取一张图片):
public class Test02 {
public static void main(String[] args) throws IOException {
//读取图片(字节数组)
byte[] imageByteArray=Files.readAllBytes(Paths.get("D:\\test\\qyqx.jpg"));
//将字节数组进行Base64编码,转换成“字符串形式”
String imageDataStr=Base64.getEncoder().encodeToString(imageByteArray);
System.out.println(imageDataStr);
//Base64解码
byte[] imageResultByteArray=Base64.getDecoder().decode(imageDataStr);
Files.write(Paths.get("D:\\image\\qyqx.jpg"), imageResultByteArray);
}
}
Base64 编码的目的是把二进制数据变成文本格式,这样在很多文本中就可以处理二进制数据。例 如,电子邮件协议就是文本协议,如果要在电子邮件中添加一个二进制文件,就可以用 Base64 编 码,然后以文本的形式传送。
Base64 编码的缺点是传输效率会降低,因为它把原始数据的长度增加了1/3。和 URL 编码一 样, Base64 编码是一种编码算法,不是加密算法。
如果把 Base64 的 64 个字符编码表换成 32 个、 48 个或者 58 个,就可以使用 Base32 编码, Base48 编码和 Base58 编码。字符越少,编码的效率就会越低。
URL编码与Base编码的总结:
URL 编码和 Base64 编码都是编码算法,它们不是加密算法;
URL 编码的目的是把任意文本数据编码为 % 前缀表示的文本,便于浏览器和服务器处理;
Base64 编码的目的是把任意二进制数据编码为文本,但编码后数据量会增加 1/3
哈希算法:
概述
哈希算法( Hash )又称摘要算法( Digest ),它的作用是:对任意一组输入数据进行计 算,得到一个固定长度的输出摘要。哈希算法的目的:为了验证原始数据是否被篡改。
哈希算法最重要的特点就是:
相同的输入一定得到相同的输出;
不同的输入大概率得到不同的输出。
哈希算法会产生哈希碰撞 ,这样的碰撞也是无法避免的因为输出的字节长度是固定的, String 的 hashCode() 输出是 4 字节整数,最多只有 42949672 96 种输出,但输入的数据长度是不固定的,有无数种输入。所以,哈希算法是把一个无限的输入集 合映射到一个有限的输出集合,必然会产生碰撞。
碰撞不可怕,我们担心的不是碰撞,而是碰撞的概率,因为碰撞概率的高低关系到哈希算法的 安全性。一个安全的哈希算法必须满足:
碰撞概率低;
不能猜测输出。
不能猜测输出是指:输入的任意一个 bit 的变化会造成输出完全不同,这样就很难从输出反推 输入(只能依靠暴力穷举)。
常用的哈希算法:
常用的哈希算法有:根据碰撞概率,哈希算法的输出长度越长,就越难产生碰撞,也就越安全。
MD5算法
MD5算法的实例:
public class Test02 {
public static void main(String[] args) {
String password="hhcbb1007";
try {
//获取基于MD5加密算法的工具对象
MessageDigest digest=MessageDigest.getInstance("MD5");
//更新原始数据
digest.update(password.getBytes());
//加密后的字节数组,转换字符串
byte[] resultByteArray=digest.digest();
StringBuilder sb=new StringBuilder();
for(byte by:resultByteArray) {
sb.append(String.format("%02x", by));
}
System.out.println(sb.length());
System.out.println(sb);
} catch (NoSuchAlgorithmException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
使用 MessageDigest 时,我们首先根据哈希算法获取一个 MessageDigest 实例,然后,反复调用 u pdate(byte[]) 输入数据。当输入结束后,调用 digest() 方法获得 byte[] 数组表示的摘要,最 后,把它转换为十六进制的字符串。
SHA-1算法
HA-1 也是一种哈希算法,它的输出是 160 bits ,即 20 字节。 SHA-1 是由美国国家安全 局开发的, SHA 算法实际上是一个系列,包括 SHA-0 (已废弃)、 SHA-1 、 SHA-256 、 SHA512 等。 在 Java 中使用 SHA-1 ,和 MD5 完全一样,只需要把算法名称改为" SHA-1 ":
SHA-1算法实例:
public class Test04 {
public static void main(String[] args) {
try {
String password="qwertyu";
String salt=UUID.randomUUID().toString().substring(0,5);
System.out.println(salt);
MessageDigest digestSalt=MessageDigest.getInstance("SHA-1");
digestSalt.update((password+salt).getBytes());//SHA-1
byte[] saltByteArray=digestSalt.digest();
StringBuilder sb=new StringBuilder();
for(byte by:saltByteArray) {
sb.append(String.format("%02x", by));
}
System.out.println(sb.length());
System.out.println("SHA-1:"+sb);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
类似的,计算 SHA-256 ,我们需要传入名称" SHA-256 ",计算 SHA-512 ,我们需要传入名称" SH A-512 "。
Hmac算法
Hmac 算法就是一种基于密钥的消息认证码算法,它的全称是 Hash-based Message Authen tication 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 。
HmacMD5代码实例:
public class Test07 {
public static void main(String[] args) {
//HmacMD5
String password="hhcbc";
try {
//秘钥连接器
KeyGenerator keyStr=KeyGenerator.getInstance("HmacMD5");
//生成秘钥
SecretKey key=keyStr.generateKey();
//获取秘钥key字节数组
byte[] resultByteKey=key.getEncoded();
System.out.println(resultByteKey.length);
StringBuilder sb=new StringBuilder();
for(byte b:resultByteKey) {
sb.append(String.format("%02x", b));
}
System.out.println("秘钥内容:"+sb);
//获取算法对象
Mac mac=Mac.getInstance("HmacMD5");
//初始化秘钥、
mac.init(key);
//更新原始内容、
mac.update(password.getBytes());
//加密
byte[] by=mac.doFinal();
System.out.println("加密结果:"+by.length);
StringBuilder HmacMD5=new StringBuilder();
for(byte b:by) {
HmacMD5.append(String.format("%02x", b));
}
System.out.println("加密后的结果:"+HmacMD5);
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
}
}
和 MD5 相比,使用 HmacMD5 的步骤是:
1 通过名称 HmacMD5 获取 KeyGenerator 实例;
2 通过 KeyGenerator 创建一个 SecretKey 实例;
3 通过名称 HmacMD5 获取 Mac 实例;
4 用 SecretKey 初始化Mac实例;
5 对 Mac 实例反复调用 update(byte[]) 输入数据;
6 调用 Mac 实例的 doFinal() 获取最终的哈希值。
有了 Hmac 计算的哈希和 SecretKey ,我们想要验证怎么办?这时, SecretKey 不能从 KeyGener ator 生成,而是从一个 byte[] 数组恢复
代码实现:
public class Test06 {
public static void main(String[] args) {
String password="qwertyuiop";
try {
//通过“秘钥的字节数组”,恢复秘钥
byte[] keyByteArray= {-102, -84, 31, 95, 62, -53, 44, -8, 41, -58, -1, 110, -72, 41, 45, -94, -6, 22, 115, -125, -23, 6, 96, -101, 90, 82, -27, -26, 69, 111, -2, -115, 53, -53, -31, -68, -52, 27, 46, -62, -113, -78, 22, -89, -35, -87, 33, 50, 33, -3, -92, 42, 3, 64, 107, -120, 60, 42, 84, 84, 23, 57, -69, -24};
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();
}
}
}
对称式加密算法
概述
对称加密算法就是传统的用一个密码就进行加密和解密。
从程序的角度看,所谓加密,就是这样一个函数,它接收密码和明文,然后输出密文:
secret = encrypt(key, message);
而解密则相反,它接收密码和密文,然后输出明文:
plain = decrypt(key, secret);
在软件开发中,常用的对称加密算法有:
密钥长度直接决定加密强度,而工作模式和填充模式可以看成是对称加密算法的参数和格式选 择。Java标准库提供的算法实现并不包括所有的工作模式和所有填充模式,但是通常我们只需要挑 选常用的使用就可以了。 最后注意, DES 算法由于密钥过短,可以在短时间内被暴力破解,所以现在已经不安全了。
使用AES加密
AES算法是目前应用最广泛的加密算法,比较常见的工作模式是ECB和CBC
ECB模式
EBC模式加密并解密:
public class Main02 {
//AEC对称加密
//ECB工作模式
public static void main(String[] args) throws Exception {
//原文
String message="HHCBB";
System.out.println("Message:"+message);
//128位秘钥=16 bytes key
byte[] key="123456qwertyuiop".getBytes();
//加密:
byte[] data=message.getBytes();
byte[] encrypted=encrypt(key, data);
System.out.println("Encrypted(加密):"+Base64.getEncoder().encodeToString(encrypted));
//解密
byte[] decrypted=decrypy(key, encrypted);
System.out.println("Decrypted(解密):"+new String(decrypted));
}
public static byte[] encrypt(byte[] key,byte[] input) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, GeneralSecurityException, BadPaddingException {
//创建密码对象,需要传入算法/工作模式/填充模式
Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");
//根据key的字节内容,恢复秘钥对象
SecretKey keySpec=new SecretKeySpec(key,"AES");
//初始化秘钥:设置加密模式ENCRYPT_MODE
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
//根据原始内容(字节),进行加密
return cipher.doFinal(input);
}
//解密:
public static byte[] decrypy(byte[] key,byte[] input) throws GeneralSecurityException, NoSuchPaddingException {
//创建密码对象,需要传入算法/工作模式/填充模式
Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");
//根据key的字节内容,恢复秘钥对象
SecretKey keySpec=new SecretKeySpec(key,"AES");
//初始化秘钥:设置加密模式DECRYPT_MODE
cipher.init(Cipher.DECRYPT_MODE, keySpec);
//根据原始内容(字节),进行解密
return cipher.doFinal(input);
}
}
Java标准库提供的对称加密接口非常简单,使用时按以下步骤编写代码:
1 根据算法名称/工作模式/填充模式获取 Cipher 实例;
2 根据算法名称初始化一个 SecretKey 实例,密钥必须是指定长度;
3 使用 SerectKey 初始化 Cipher 实例,并设置加密或解密模式;
4 传入明文或密文,获得密文或明文。
CBC模式
ECB 模式是最简单的 AES 加密模式,它只需要一个固定长度的密钥,固定的明文会生成固定的密 文,这种一对一的加密方式会导致安全性降低,更好的方式是通过 CBC 模式,它需要一个随机数作 为 IV 参数,这样对于同一份明文,每次生成的密文都不同:
//CBC工作模式
public class Main03 {
//AEC对称加密
//ECB工作模式
public static void main(String[] args) throws Exception {
//原文
String message="HHCBB";
System.out.println("Message:"+message);
//256位秘钥=16 bytes key
byte[] key="123456qwertyuiopasdfghjklzxcvbnm".getBytes();
//加密:
byte[] data=message.getBytes();
byte[] encrypted=encrypt(key, data);
System.out.println("Encrypted(加密):"+Base64.getEncoder().encodeToString(encrypted));
//解密
byte[] decrypted=decrypy(key, encrypted);
System.out.println("Decrypted(解密):"+new String(decrypted));
}
//加密
public static byte[] encrypt(byte[] key,byte[] input) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, GeneralSecurityException, BadPaddingException {
//创建密码对象,需要传入算法/工作模式/填充模式
Cipher cipher=Cipher.getInstance("AES/CBC/PKCS5Padding");
//根据key的字节内容,恢复秘钥对象
SecretKey keySpec=new SecretKeySpec(key,"AES");
//CBC模式需要生成一个16bytes的initialization vector
SecureRandom sr=SecureRandom.getInstanceStrong();
byte[] iv=sr.generateSeed(16);
System.out.println(Arrays.toString(iv));
IvParameterSpec ivps=new IvParameterSpec(iv);
//初始化秘钥:设置加密模式ENCRYPT_MODE
cipher.init(Cipher.ENCRYPT_MODE, keySpec,ivps);
//加密
byte[] data=cipher.doFinal(input);
//根据原始内容(字节),进行加密
return join(iv,data);
}
//解密:
public static byte[] decrypy(byte[] key,byte[] input) throws GeneralSecurityException, NoSuchPaddingException {
//把input分割成IV和密文
byte[] iv=new byte[16];
byte[] data=new byte[input.length-16];
System.arraycopy(input, 0, iv, 0, 16);
System.arraycopy(input, 16, data, 0, data.length);
System.out.println(Arrays.toString(iv));
//创建密码对象,需要传入算法/工作模式/填充模式
Cipher cipher=Cipher.getInstance("AES/CBC/PKCS5Padding");
//根据key的字节内容,恢复秘钥对象
SecretKey keySpec=new SecretKeySpec(key,"AES");
//恢复iv
IvParameterSpec ivps=new IvParameterSpec(iv);
//初始化秘钥:设置加密模式DECRYPT_MODE
cipher.init(Cipher.DECRYPT_MODE, keySpec,ivps);
//根据原始内容(字节),进行解密
return cipher.doFinal(data );
}
public static byte[] join(byte[] bs1,byte[] bs2) {
byte[] r=new byte[bs1.length+bs2.length];
System.arraycopy(bs1, 0, r, 0, bs1.length);
System.arraycopy(bs2, 0, r, bs1.length, bs2.length);
return r;
}
}
在 CBC 模式下,需要一个随机生成的 16 字节IV参数,必须使用 SecureRandom 生成。因为多 了一个 IvParameterSpec 实例,因此,初始化方法需要调用 Cipher 的一个重载方法并传入 IvPara meterSpec 。
观察输出,可以发现每次生成的 IV 不同,密文也不同
非对称式加密算法
概述
从 DH 算法我们可以看到,公钥-私钥组成的密钥对是非常有用的加密方式,因为公钥是可以公 开的,而私钥是完全保密的,由此奠定了非对称加密的基础。
非对称加密:加密和解密使用的不是相同的密钥,只有同一个公钥-私钥对才能正常加解密。
非对称加密的优点:对称加密需要协商密钥,而非对称加密可以安全地公开各自的公钥,在N个 人之间通信的时候:使用非对称加密只需要N个密钥对,每个人只管理自己的密钥对。而使用对称加 密需要则需要N*(N-1)/2个密钥,因此每个人需要管理N-1个密钥,密钥管理难度大,而且非常容易泄 漏。
非对称加密的缺点:运算速度非常慢,比对称加密要慢很多。 所以,在实际应用的时候,非对称加密总是和对称加密一起使用。
RSA算法
非对称加密的代码实例:
// RSA
public class Main05 {
public static void main(String[] args) throws Exception {
// 明文:
byte[] plain = "Hello, encrypt use RSA".getBytes("UTF-8");
// 创建公钥/私钥对:
Human alice = new Human("Alice");
// 用Alice的公钥加密:
// 获取Alice的公钥,并输出
byte[] pk = alice.getPublicKey();
System.out.println(String.format("public key(公钥): %x", new BigInteger(1, pk)));
// 使用公钥加密
byte[] encrypted = alice.encrypt(plain);
System.out.println(String.format("encrypted: %x", new BigInteger(1, encrypted)));
// 用Alice的私钥解密:
// 获取Alice的私钥,并输出
byte[] sk = alice.getPrivateKey();
System.out.println(String.format("private key: %x", new BigInteger(1, sk)));
// 使用私钥解密
byte[] decrypted = alice.decrypt(encrypted);
System.out.println(new String(decrypted, "UTF-8"));
}
}
// 用户类
class Human {
// 姓名
String name;
// 私钥:
PrivateKey sk;
// 公钥:
PublicKey pk;
// 构造方法
public Human(String name) throws GeneralSecurityException {
// 初始化姓名
this.name = name;
// 生成公钥/私钥对:
KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA");
kpGen.initialize(1024);
KeyPair kp = kpGen.generateKeyPair();
this.sk = kp.getPrivate();
this.pk = kp.getPublic();
}
// 把私钥导出为字节
public byte[] getPrivateKey() {
return this.sk.getEncoded();
}
// 把公钥导出为字节
public byte[] getPublicKey() {
return this.pk.getEncoded();
}
// 用公钥加密:
public byte[] encrypt(byte[] message) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, this.pk); // 使用公钥进行初始化
return cipher.doFinal(message);
}
// 用私钥解密:
public byte[] decrypt(byte[] input) throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, this.sk); // 使用私钥进行初始化
return cipher.doFinal(input);
}
}
RSA 的公钥和私钥都可以通过 getEncoded() 方法获得以 byte[] 表示的二进制数据,并根据需要 保存到文件中。要从 byte[] 数组恢复公钥或私钥,可以这么写:
byte[] pkData = ...
byte[] skData = ...
KeyFactory kf = KeyFactory.getInstance("RSA");
// 恢复公钥:
X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(pkData);
PublicKey pk = kf.generatePublic(pkSpec);
// 恢复私钥:
PKCS8EncodedKeySpec skSpec = new PKCS8EncodedKeySpec(skData);
PrivateKey sk = kf.generatePrivate(skSpec);
以 RSA 算法为例,它的密钥有 256 / 512 / 1024 / 2048 / 4096 等不同的长度。长度越长,密 码强度越大,当然计算速度也越慢。
小结:对称式加密算法与非对称式加密算法的比较:
对称加密算法使用同一个密钥进行加密和解密,常用算法有 DES 、 AES 和 IDEA 等;
使用对称加密算法需要指定算法名称、工作模式和填充模式。
非对称加密:加密和解密使用的不是相同的密钥,只有同一个公钥-私钥对才能正常加解密。