目錄
一、URL編碼:
二、Base64編碼:
三、哈希算法:
四、Hmac算法:
五、對稱加密算法:
一、URL編碼:
URL
編碼是瀏覽器發送數據給服務器時使用的編碼,它通常附加在URL
的參數部分。之所以需要URL
編碼,是因為出于兼容性考慮,很多服務器只識別ASCII
字符。但如果URL
中包含中文、日文這些非ASCII
字符怎么辦?不要緊,URL
編碼有一套規則:
- 如果字符是
A
~Z
,a
~z
,0
~9
以及-
、_
、.
、*
,則保持不變; - 如果是其他字符,先轉換為
UTF-8
編碼,然后對每個字節以%XX
表示。
例如:字符"中"的UTF-8
編碼是0xe4b8ad
,因此,它的URL
編碼是%E4%B8%AD
。URL
編碼總是大寫。
Java
標準庫提供了一個URLEncoder
類來對任意字符串進行URL
編碼:
import java.net.URLEncoder;
public class Main {public static void main(String[] args) {String encoded = URLEncoder.encode("中文!", "utf-8");System.out.println(encoded);}
}
URL
編碼是編碼算法,不是加密算法。URL
編碼的目的是把任意文本數據編碼為%
前綴表示的文本,編碼后的文本僅包含A
~Z
,a
~z
,0
~9
,-
,_
,.
,*
和%
,便于瀏覽器和服務器處理。
二、Base64編碼:
Base64
編碼是對二進制數據進行編碼,表示成文本格式。Base64
編碼可以把任意長度的二進制數據變為純文本,并且純文本內容中且只包含指定字符內容:A
~Z
、a
~z
、0
~9
、+
、/
、=
。它的原理是把3
字節的二進制數據按6bit
一組,用4
個整數表示,然后查表,把整數用索引對應到字符,得到編碼后的字符串。6
位整數的范圍總是0
~63
,所以,能用64
個字符表示:字符A
~Z
對應索引0
~25
,字符a
~z
對應索引26
~51
,字符0
~9
對應索引52
~61
,最后兩個索引62
、63
分別用字符+
和/
表示。
base64碼表:
碼值 | 字符 | 碼值 | 字符 | 碼值 | 字符 | 碼值 | 字符 |
---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w |
1 | B | 17 | R | 33 | h | 49 | x |
2 | C | 18 | S | 34 | i | 50 | y |
3 | D | 19 | T | 35 | j | 51 | z |
4 | E | 20 | U | 36 | k | 52 | 0 |
5 | F | 21 | V | 37 | l | 53 | 1 |
6 | G | 22 | W | 38 | m | 54 | 2 |
7 | H | 23 | X | 39 | n | 55 | 3 |
8 | I | 24 | Y | 40 | o | 56 | 4 |
9 | J | 25 | Z | 41 | p | 57 | 5 |
10 | K | 26 | a | 42 | q | 58 | 6 |
11 | L | 27 | b | 43 | r | 59 | 7 |
12 | M | 28 | c | 44 | s | 60 | 8 |
13 | N | 29 | d | 45 | t | 61 | 9 |
14 | O | 30 | e | 46 | u | 62 | + |
15 | P | 31 | f | 47 | v | 63 | / |
在Java
中,二進制數據就是byte[]
數組。Java
標準庫提供了Base64
來對byte[]
數組進行編解碼:
public class Main {public static void main(String[] args) {byte[] input = new byte[] { (byte) 0xe4, (byte) 0xb8, (byte) 0xad };String b64encoded = Base64.getEncoder().encodeToString(input);System.out.println(b64encoded);}
}
編碼后得到字符串結果:5Lit
。要對這個字符使用Base64
解碼,仍然用Base64
這個類:
public class Main {public static void main(String[] args) {byte[] output = Base64.getDecoder().decode("5Lit");System.out.println(Arrays.toString(output)); // [-28, -72, -83]}
}
三、哈希算法:
哈希算法(Hash
)又稱摘要算法(Digest
),它的作用是:對任意一組輸入數據進行計算,得到一個固定長度的輸出摘要。哈希算法最重要的特點就是:
- 相同的輸入一定得到相同的輸出;
- 不同的輸入大概率得到不同的輸出。
所以,哈希算法的目的:為了驗證原始數據是否被篡改。Java
字符串的hashCode()
就是一個哈希算法,它的輸入是任意字符串,輸出是固定的4
字節int
整數:
"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f
?1、哈希碰撞:兩個不同的輸入得到了相同的輸出。(不能避免,安全性低)
"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0"通話".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03
2、常用的哈希算法:
算法 | 輸出長度(位) | 輸出長度(字節) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1、MD5算法:
使用MessageDigest
時,我們首先根據哈希算法獲取一個MessageDigest
實例,然后,反復調用update(byte[])
輸入數據。當輸入結束后,調用digest()
方法獲得byte[]
數組表示的摘要,最后,把它轉換為十六進制的字符串。
import java.security.MessageDigest;public class main {public static void main(String[] args) {// 創建一個MessageDigest實例:MessageDigest md = MessageDigest.getInstance("MD5");// 反復調用update輸入數據:md.update("Hello".getBytes("UTF-8"));md.update("World".getBytes("UTF-8"));// 16 bytes: 68e109f0f40ca72a15e05cc22786f8e6byte[] results = md.digest(); StringBuilder sb = new StringBuilder();for(byte bite : results) {sb.append(String.format("%02x", bite));}System.out.println(sb.toString());}
}
2、哈希算法的用途:
(1)校驗下載文件
(2)存儲用戶密碼:(要避免彩虹表攻擊,對每個口令額外添加隨機數,這個方法稱之為加鹽(salt
)digest = md5(salt + inputPassword)
)
3、SHA-1算法:
SHA-1
也是一種哈希算法,它的輸出是160 bits
,即20
字節。SHA-1
是由美國國家安全局開發的,SHA
算法實際上是一個系列,包括SHA-0
(已廢棄)、SHA-1
、SHA-256
、SHA-512
等。在Java
中使用SHA-1
,和MD5
完全一樣,只需要把算法名稱改為"SHA-1
":
import java.security.MessageDigest;public class main {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: db8ac1c259eb89d4a131b253bacfca5f319d54f2byte[] results = md.digest(); StringBuilder sb = new StringBuilder();for(byte bite : results) {sb.append(String.format("%02x", bite));}System.out.println(sb.toString());}
}
4、RipeMD160:
Java
標準庫的java.security
包提供了一種標準機制,允許第三方提供商無縫接入。我們要使用BouncyCastle
提供的RipeMD160
算法,需要先把BouncyCastle
注冊一下
public class Main {public static void main(String[] args) throws Exception {// 注冊BouncyCastle提供的通知類對象BouncyCastleProviderSecurity.addProvider(new BouncyCastleProvider());// 獲取RipeMD160算法的"消息摘要對象"(加密對象)MessageDigest md = MessageDigest.getInstance("RipeMD160");// 更新原始數據md.update("HelloWorld".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算法:
mac
算法就是一種基于密鑰的消息認證碼算法,它的全稱是Hash-based Message Authentication Code
,是一種更安全的消息摘要算法。Hmac
算法總是和某種哈希算法配合起來用的。例如我們使用MD5
算法,對應的就是Hmac MD5
算法,它相當于“加鹽”的MD5
:HmacMD5 ≈md5(secure_random_key, input)
HmacMD5加密:
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;public class main {public static void main(String[] args) throws NoSuchAlgorithmException, IllegalStateException, UnsupportedEncodingException, InvalidKeyException {// 獲取HmacMD5秘鑰生成器KeyGenerator keyGen = KeyGenerator.getInstance("HmacMD5");// 產生秘鑰SecretKey secreKey = keyGen.generateKey();// 打印隨機生成的秘鑰:byte[] keyArray = secreKey.getEncoded();StringBuilder key = new StringBuilder();for(byte bite:keyArray) {key.append(String.format("%02x", bite));}System.out.println(key);// 使用HmacMD5加密Mac mac = Mac.getInstance("HmacMD5");mac.init(secreKey); // 初始化秘鑰mac.update("HelloWorld".getBytes("UTF-8"));byte[] resultArray = mac.doFinal();StringBuilder result = new StringBuilder();for(byte bite:resultArray) {result.append(String.format("%02x", bite));}System.out.println(result);}
}
解密:
// 原始密碼
String password = "nhmyzgq";// 通過"秘鑰的字節數組",恢復秘鑰
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);
五、對稱加密算法:
對稱加密算法就是傳統的用一個秘鑰進行加密和解密。例如,我們常用的WinZIP
和WinRAR
對壓縮包的加密和解密,就是使用對稱加密算法:
從程序的角度看,所謂加密,就是這樣一個函數,它接收密碼和明文,然后輸出密文:
secret = encrypt(key, message);
而解密則相反,它接收密碼和密文,然后輸出明文:
plain = decrypt(key, secret);
在軟件開發中,常用的對稱加密算法有:
算法 | 密鑰長度 | 工作模式 | 填充模式 |
DES | 56/64 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/... |
AES | 128/192/256 | ECB/CBC/PCBC/CTR/... | NoPadding/PKCS5Padding/PKCS7Padding/... |
IDEA | 128 | ECB | PKCS5Padding/PKCS7Padding/... |
?
密鑰長度直接決定加密強度,而工作模式和填充模式可以看成是對稱加密算法的參數和格式選擇。Java
標準庫提供的算法實現并不包括所有的工作模式和所有填充模式。
1、使用AES加密:
(1)ECB模式:ECB
模式是最簡單的AES
加密模式,它需要一個固定長度的密鑰,固定的明文會生成固定的密文。
import java.security.*;
import java.util.Base64;import javax.crypto.*;
import javax.crypto.spec.*;public class Main {public static void main(String[] args) throws Exception {// 原文:String message = "Hello, world!";System.out.println("Message(原始信息): " + message);// 128位密鑰 = 16 bytes Key:byte[] key = "1234567890abcdef".getBytes();// 加密:byte[] data = message.getBytes();byte[] encrypted = encrypt(key, data);System.out.println("Encrypted(加密內容): " + Base64.getEncoder().encodeToString(encrypted));// 解密:byte[] decrypted = decrypt(key, encrypted);System.out.println("Decrypted(解密內容): " + new String(decrypted));}// 加密:public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {// 創建密碼對象,需要傳入算法/工作模式/填充模式Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");// 根據key的字節內容,"恢復"秘鑰對象SecretKey keySpec = new SecretKeySpec(key, "AES");// 初始化秘鑰:設置加密模式ENCRYPT_MODEcipher.init(Cipher.ENCRYPT_MODE, keySpec);// 根據原始內容(字節),進行加密return cipher.doFinal(input);}// 解密:public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {// 創建密碼對象,需要傳入算法/工作模式/填充模式Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");// 根據key的字節內容,"恢復"秘鑰對象SecretKey keySpec = new SecretKeySpec(key, "AES");// 初始化秘鑰:設置解密模式DECRYPT_MODEcipher.init(Cipher.DECRYPT_MODE, keySpec);// 根據原始內容(字節),進行解密return cipher.doFinal(input);}
}
Java
標準庫提供的對稱加密接口非常簡單,使用時按以下步驟編寫代碼:
(1)根據算法名稱/工作模式/填充模式獲取Cipher
實例;
(2)根據算法名稱初始化一個SecretKey
實例,密鑰必須是指定長度;
(3)使用SerectKey
初始化Cipher
實例,并設置加密或解密模式;
(4)傳入明文或密文,獲得密文或明文。
2、CBC模式:ECB
模式是最簡單的AES
加密模式,這種一對一的加密方式會導致安全性降低。所以,更好的方式是通過CBC
模式,它需要一個隨機數作為IV
參數,這樣對于同一份明文,每次生成的密文都不同:
import java.security.*;
import java.util.Base64;import javax.crypto.*;
import javax.crypto.spec.*;public class Main {public static void main(String[] args) throws Exception {// 原文:String message = "Hello, world!";System.out.println("Message(原始信息): " + message);// 256位密鑰 = 32 bytes Key:byte[] key = "1234567890abcdef1234567890abcdef".getBytes();// 加密:byte[] data = message.getBytes();byte[] encrypted = encrypt(key, data);System.out.println("Encrypted(加密內容): " + Base64.getEncoder().encodeToString(encrypted));// 解密:byte[] decrypted = decrypt(key, encrypted);System.out.println("Decrypted(解密內容): " + new String(decrypted));}// 加密:public static byte[] encrypt(byte[] key, byte[] input) throws GeneralSecurityException {// 設置算法/工作模式CBC/填充Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");// 恢復秘鑰對象SecretKeySpec keySpec = new SecretKeySpec(key, "AES");// CBC模式需要生成一個16 bytes的initialization vector:SecureRandom sr = SecureRandom.getInstanceStrong();byte[] iv = sr.generateSeed(16); // 生成16個字節的隨機數System.out.println(Arrays.toString(iv));IvParameterSpec ivps = new IvParameterSpec(iv); // 隨機數封裝成IvParameterSpec參數對象// 初始化秘鑰:操作模式、秘鑰、IV參數cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivps);// 加密byte[] data = cipher.doFinal(input);// IV不需要保密,把IV和密文一起返回:return join(iv, data);}// 解密:public static byte[] decrypt(byte[] key, byte[] input) throws GeneralSecurityException {// 把input分割成IV和密文:byte[] iv = new byte[16];byte[] data = new byte[input.length - 16];System.arraycopy(input, 0, iv, 0, 16); // IVSystem.arraycopy(input, 16, data, 0, data.length); //密文System.out.println(Arrays.toString(iv));// 解密:Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); // 密碼對象SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); // 恢復秘鑰IvParameterSpec ivps = new IvParameterSpec(iv); // 恢復IV// 初始化秘鑰:操作模式、秘鑰、IV參數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;}
}