目錄
一.網絡編程基礎
1. 概述
2. IP地址
?3. 域名
?4. 網絡模型
5. 常用協議
6. 小結
二.TCP編程
1. 什么是Socket?
2. 服務器端
3. 客戶端
4. Socket流
5. 小結
三.UDP編程
1. 概述
2. 服務器端
3. 客戶端
4. 小結
案例:
四.加密算法
1. 什么是編碼?
2. Base64編碼
3. 小結
五.哈希算法
1. 概述
2. 哈希碰撞
3. 常用哈希算法
4. 哈希算法的用途
4.1. 校驗下載文件
4.2. 存儲用戶密碼
5. SHA-1
6. 小結
例題:
六.Hmac算法
1. 概述
2. 小結
案例:
登錄加密,和二次登錄,獲取key驗證
七.對稱加密算法
1. 概述
2. 使用AES加密
2.1. ECB模式
2.2. CBC模式
3. 小結
一.網絡編程基礎
1. 概述
????????計算機網絡是指兩臺或更多的計算機組成的網絡,在同一個網絡中,任意兩臺計算機都可以直接通信,因為所有計算機都需要遵循同一種網絡協議。
????????那什么是互聯網呢?互聯網是網絡的網絡(internet
),即把很多計算機網絡連接起來,形成一個全球統一的互聯網。對某個特定的計算機網絡來說,它可能使用網絡協議ABC
,而另一個計算機網絡可能使用網絡協議XYZ
。如果計算機網絡各自的通訊協議不統一,就沒法把不同的網絡連接起來形成互聯網。因此,為了把計算機網絡接入互聯網,就必須使用TCP/IP
協議。
? ?TCP/IP
協議泛指互聯網協議,其中最重要的兩個協議是TCP協議
和IP協議
。只有使用TCP/IP協議
的計算機才能夠聯入互聯網,使用其他網絡協議(例如NetBIOS
、AppleTalk
協議等)是無法聯入互聯網的。
中國四大主流網絡體系是Chinanet
CERNET
cstnet
CHINAGBN
。
Chinanet
是郵電部門經營管理的基于Internet
網絡技術的中國公用計算機互聯網,是國際計算機互聯網(Internet
)的一部分,是中國的Internet
骨干網。CERNET
中國教育和科研計算機網CERNET
是由國家投資建設,教育部負責管理,清華大學等高等學校承擔建設和管理運行的全國性學術計算機互聯網絡。cstnet
1994年中國科學技術網CSTNET
首次實現和Internet
直接連接,同時建立了中國最高域名服務器,標志著中國正式接入Internet
。ChinaGBN
(China Golden Bridge Network)也稱做中國國家公用經濟信息通信網。它是中國國民經濟信息化的基礎設施,是建立金橋工程的業務網,支持金關、金稅、金卡等“金”字頭工程的應用。
2. IP地址
????????在互聯網中,一個IP
地址用于唯一標識一個網絡接口(Network Interface
)。一臺聯入互聯網的計算機肯定有一個IP
地址,但也可能有多個IP
地址(多個網卡)。
??IP
地址分為IPv4
和IPv6
兩種。IPv4
采用32
位地址,類似101.202.99.12
,而IPv6
采用128
位地址,類似2001:0DA8:100A:0000:0000:1020:F2F3:1428
。IPv4
地址總共有232
個(大約42億),而IPv6
地址則總共有2128
個(大約340萬億億億億),IPv4
的地址目前已耗盡,而IPv6
的地址是根本用不完的。
????????IP地址又分為公網IP地址
和內網IP地址
。公網IP地址可以直接被訪問,內網IP
地址只能在內網訪問。內網IP
地址類似于:
-
192.168.x.x
10.x.x.x
有一個特殊的IP地址,稱之為本機地址,它總是127.0.0.1
。
1707762444 = 0x65ca630c= 65 ca 63 0c= 101.202.99.12
每臺計算機都需要正確配置IP地址
和子網掩碼
,根據這兩個就可以計算網絡號,如果兩臺計算機計算出的網絡號相同,說明兩臺計算機在同一個網絡,可以直接通信。如果兩臺計算機計算出的網絡號不同,那么兩臺計算機不在同一個網絡,不能直接通信,它們之間必須通過路由器或者交換機這樣的網絡設備間接通信,我們把這種設備稱為網關
。
網關
的作用就是連接多個網絡,負責把來自一個網絡的數據包發到另一個網絡,這個過程叫路由。所以,一臺計算機的一個網卡會有3個關鍵配置:
-
IP
地址,例如:10.0.2.15
- 子網掩碼,例如:
255.255.255.0
- 網關的IP地址,例如:
10.0.2.2
有一個特殊的本機域名localhost
,它對應的IP地址總是本機地址127.0.0.1
。
在Windows
操作系統中,可以通過ipconfig
命令查看本地主機的IP
地址:
InetAddress localIPAddress = InetAddress.getLocalHost();
System.out.println("本地主機IP:" + localIPAddress.getHostAddress());
System.out.println("本地主機名稱:" + localIPAddress.getHostName());
?如果想檢查當前主機與目標主機之間的網絡是否通暢,可以使用ping
命令來進行測試:
?在Java
中,如果需要測試網絡是否通暢,可以使用Runtime
對象exec()
執行ping
命令:
Process process = Runtime.getRuntime().exec("ping 192.168.254.162");BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line = null;
while((line = reader.readLine()) != null) {System.out.println(line);
}
?3. 域名
因為直接記憶IP地址非常困難,所以我們通常使用域名
訪問某個特定的服務。域名解析服務器DNS
負責把域名
翻譯成對應的IP
,客戶端再根據IP地址訪問服務器。
用nslookup
可以查看域名對應的IP地址:
?4. 網絡模型
由于計算機網絡從底層的傳輸到高層的軟件設計十分復雜,要合理地設計計算機網絡模型,必須采用分層模型,每一層負責處理自己的操作。OSI
(Open System Interconnect)網絡模型是ISO
組織定義的一個計算機互聯的標準模型,注意它只是一個定義,目的是為了簡化網絡各層的操作,提供標準接口便于實現和維護。這個模型從上到下依次是:
- 應用層,提供應用程序之間的通信;
- 表示層:處理數據格式,加解密等等;
- 會話層:負責建立和維護會話;
- 傳輸層:負責提供端到端的可靠傳輸;
- 網絡層:負責根據目標地址選擇路由來傳輸數據;
- 數據鏈路層和物理層:負責把數據進行分片并且真正通過物理網絡傳輸,例如,無線網、光纖等。
互聯網實際使用的TCP/IP
模型并不是對應到OSI
的7層模型
,而是大致對應OSI
的5層模型
:
5. 常用協議
???IP協議
是一個分組交換協議,它不保證可靠傳輸。而TCP協議
是傳輸控制協議,它是面向連接
的協議,支持可靠傳輸
和雙向通信
。TCP協議
是建立在IP協議之上的,簡單地說,IP協議只負責發數據包,不保證順序和正確性,而TCP協議負責控制數據包傳輸,它在傳輸數據之前需要先建立連接,建立連接后才能傳輸數據,傳輸完后還需要斷開連接。TCP協議之所以能保證數據的可靠傳輸,是通過接收確認
、超時重傳
這些機制實現的。并且,TCP協議允許雙向通信,即通信雙方可以同時發送和接收數據。
????????TCP協議也是應用最廣泛的協議,許多高級協議都是建立在TCP協議之上的,例如HTTP
、SMTP
等。
??UDP協議
(User Datagram Protocol)是一種數據報文協議,它是無連接協議
,不保證可靠傳輸
。因為UDP
協議在通信前不需要建立連接,因此它的傳輸效率比TCP
高,而且UDP
協議比TCP
協議要簡單得多。選擇UDP
協議時,傳輸的數據通常是能容忍丟失的,例如,一些語音視頻通信的應用會選擇UDP
協議。
6. 小結
計算機網絡: 軟件體系: cs Client/server bs Browser/server 網絡編程三要素 1.IP地址: ip:IPV4:32位地址長度,每八位一組,分成四組 x-0~255 ip:IPV6:128位地址長度,每十六位一組,分成八組 x-0~65535 2001:0DA8::1020:F2F3:1428 2.端口號:應用程序在設備中的唯一標識,端口號由二個字節0~65535,0-1023端口號(被廣大服務商占用),1024以上可用 3.協議: TCP/UDP: TCP:面向連接的協議 UDP:無連接的協議
二.TCP編程
1. 什么是Socket?
????????在開發網絡應用程序的時候,會遇到Socket
這個概念。Socket
是一個抽象概念,一個應用程序通過一個Socket
來建立一個遠程連接,而Socket
內部通過TCP/IP
協議把數據傳輸到網絡。
┌───────────┐ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ┌───────────┐
│Application│? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │Application│
├───────────┤ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ├───────────┤
│ ?Socket ? │? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? │ ?Socket ? │
├───────────┤ ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ├───────────┤
│ ? ?TCP ? ?│? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?│ ? ?TCP ? ?│
├───────────┤ ? ? ?┌──────┐ ? ? ? ┌──────┐ ? ? ?├───────────┤
│ ? ?IP ? ? │<────>│Router│<─────>│Router│<────>│ ? ?IP ? ? │
└───────────┘ ? ? ?└──────┘ ? ? ? └──────┘ ? ? ?└───────────┘
Socket
、TCP
和部分IP
的功能都是由操作系統提供的,不同的編程語言只是提供了對操作系統調用的簡單的封裝。例如:Java
提供的幾個Socket
相關的類就封裝了操作系統提供的接口:ServerSocket
類、Socket
類。
為什么需要Socket
進行網絡通信?因為僅僅通過IP
地址進行通信是不夠的,同一臺計算機同一時間會運行多個網絡應用程序,例如瀏覽器、QQ、郵件客戶端等。當操作系統接收到一個數據包的時候,如果只有IP
地址,它沒法判斷應該發給哪個應用程序,所以,操作系統抽象出Socket
接口,每個應用程序需要各自對應到不同的Socket
,數據包才能根據Socket
正確地發到對應的應用程序。
一個Socket
就是由IP
地址和端口號(范圍是0~65535
)組成,可以把Socket
簡單理解為IP
地址+端口號。端口號總是由操作系統分配,它是一個0
~65535
之間的數字,其中,小于1024
的端口屬于特權端口,需要管理員權限,大于1024
的端口可以由任意用戶的應用程序打開。
所以,如果需要與指定主機進行通信,完整的通信地址是由一個IP
地址+端口號組成:
101.202.99.2:1201
101.202.99.2:1304
101.202.99.2:15000
使用Socket
進行網絡編程時,本質上就是兩個進程之間的網絡通信。其中一個進程必須充當服務器端,它會主動監聽某個指定的端口,另一個進程必須充當客戶端,它必須主動連接服務器的IP
地址和指定端口,如果連接成功,服務器端和客戶端就成功地建立了一個TCP
連接,雙方后續就可以隨時發送和接收數據。
因此,當Socket
連接成功地在服務器端和客戶端之間建立后:
- 對服務器端來說:它的
Socket
是指定的IP
地址和指定的端口號; - 對客戶端來說:它的
Socket
是它所在計算機的IP
地址和一個由操作系統分配的隨機端口號。
2. 服務器端
public class Server {public static void main(String[] args) throws IOException {ServerSocket ss = new ServerSocket(6666); // 監聽指定端口System.out.println("server is running...");while (true) {Socket sock = ss.accept();// 使用Socket流進行網絡通信// ...System.out.println("connected from " + sock.getRemoteSocketAddress());}}
}
服務器端通過下述代碼,在指定端口6666
監聽。這里我們沒有指定IP
地址,表示在計算機的所有網絡接口上進行監聽。
ServerSocket ss = new ServerSocket(6666);
如果ServerSocket
監聽成功,我們就使用一個無限循環來處理客戶端的連接,注意到代碼ss.accept()
表示每當有新的客戶端連接進來后,就返回一個Socket
實例,這個Socket
實例就是用來和剛連接的客戶端進行通信的。
while (true) {Socket sock = ss.accept();System.out.println("connected from " + sock.getRemoteSocketAddress());}
如果沒有客戶端連接進來,accept()
方法會阻塞并一直等待。如果有多個客戶端同時連接進來,ServerSocket
會把連接扔到隊列里,然后一個一個處理。對于Java
程序而言,只需要通過循環不斷調用accept()
就可以獲取新的連接。
3. 客戶端
相比服務器端,客戶端程序就要簡單很多。一個典型的客戶端程序如下:
public class Client {public static void main(String[] args) throws IOException {// 連接指定服務器和端口Socket sock = new Socket("localhost", 6666); // 使用Socket流進行網絡通信// ...// 關閉sock.close();System.out.println("disconnected.");}
}
?客戶端程序通過下述代碼,連接到服務器端,注意上述代碼的服務器地址是"localhost
",表示本機地址,端口號是6666
。如果連接成功,將返回一個Socket
實例,用于后續通信。
Socket sock = new Socket("localhost", 6666);
4. Socket流
當Socket
連接創建成功后,無論是服務器端,還是客戶端,我們都使用Socket
實例進行網絡通信。因為TCP
是一種基于流的協議,因此,Java
標準庫使用InputStream
和OutputStream
來封裝Socket
的數據流,這樣我們使用Socket
的流,和普通IO
流類似:
// 用于讀取網絡數據:
InputStream in = sock.getInputStream();// 用于寫入網絡數據:
OutputStream out = sock.getOutputStream();
寫入網絡數據時,必須要調用flush()方法。如果不調用flush(),我們很可能會發現,客戶端和服務器都收不到數據,這并不是Java標準庫的設計問題,而是我們以流的形式寫入數據的時候,并不是一寫入就立刻發送到網絡,而是先寫入內存緩沖區,直到緩沖區滿了以后,才會一次性真正發送到網絡,這樣設計的目的是為了提高傳輸效率。如果緩沖區的數據很少,而我們又想強制把這些數據發送到網絡,就必須調用flush()強制把緩沖區數據發送出去。
5. 小結
使用Java進行TCP
編程時,需要使用Socket
模型:
-
- 服務器端用
ServerSocket
監聽指定端口; - 客戶端使用
Socket(InetAddress, port)
連接服務器; - 服務器端用
accept()
接收連接并返回Socket
實例; - 雙方通過
Socket
打開InputStream
/OutputStream
讀寫數據; - 服務器端通常使用多線程同時處理多個客戶端連接,利用線程池可大幅提升效率;
flush()
方法用于強制輸出緩沖區到網絡。
- 服務器端用
三.UDP編程
1. 概述
????????和TCP
編程相比,UDP
編程就簡單得多,因為UDP
沒有創建連接,數據包也是一次收發一個,所以沒有流的概念。
在Java
中使用UDP
編程,仍然需要使用Socket
,因為應用程序在使用UDP
時必須指定網絡接口(IP地址
)和端口號。注意:UDP
端口和TCP
端口雖然都使用0
~65535
,但他們是兩套獨立的端口,即一個應用程序用TCP
協議占用了端口1234
,不影響另一個應用程序用UDP
協議占用端口1234
。
2. 服務器端
????????在服務器端,使用UDP
也需要監聽指定的端口。Java提供了DatagramSocket
來實現這個功能,代碼如下:
DatagramSocket ds = new DatagramSocket(6666); // 監聽指定端口
while (true) { // 無限循環// 數據緩沖區:byte[] buffer = new byte[1024];DatagramPacket packet = new DatagramPacket(buffer, buffer.length);ds.receive(packet); // 收取一個UDP數據包// 收取到的數據存儲在buffer中,由packet.getOffset(), packet.getLength()指定起始位置和長度// 將其按UTF-8編碼轉換為String:String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);// 發送數據:byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);packet.setData(data);ds.send(packet);
}
服務器端首先使用如下語句在指定的端口監聽UDP
數據包:
DatagramSocket ds = new DatagramSocket(6666);
?如果沒有其他應用程序占據這個端口,那么代表監聽成功。為了能夠反復處理數據,我們使用一個死循環來處理收到的UDP
數據包:
while (true) { // 死循環}
要接收一個UDP
數據包,需要準備一個byte[]
緩沖區,并通過DatagramPacket
實現接收:
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
假設我們收取到的是一個String
,那么,通過DatagramPacket
返回的packet.getOffset()
和packet.getLength()
確定數據在緩沖區的起止位置:
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
當服務器收到一個DatagramPacket
后,通常必須立刻回復一個或多個UDP
包,因為客戶端地址在DatagramPacket
中,每次收到的DatagramPacket
可能是不同的客戶端,如果不回復,客戶端就收不到任何UDP
包。
發送UDP
包也是通過DatagramPacket
實現的:
byte[] data = ...
packet.setData(data);
ds.send(packet);
3. 客戶端
和服務器端相比,客戶端使用UDP
時,只需要直接向服務器端發送UDP
包,然后接收返回的UDP
包:
DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 6666); // 連接指定服務器和端口// 發送:
byte[] data = "Hello".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);// 接收:
byte[] buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
ds.disconnect();
客戶端打開一個DatagramSocket
使用以下代碼:
DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 6666);
4. 小結
- 使用
UDP
協議通信時,服務器和客戶端雙方無需建立連接; - 服務器端用
DatagramSocket(port)
監聽端口; - 客戶端使用
DatagramSocket.connect()
指定遠程地址和端口; - 雙方通過
receive()
和send()
讀寫數據; DatagramSocket
沒有IO
流接口,數據被直接寫入byte[]
緩沖區;
案例:
從磁盤拿出一張照片,客戶端發出去,服務端接收,并保存。返回給客戶端一個上傳成功。(判斷下大小2mb,并且必須.png結尾)
Server
package homework;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
import java.util.UUID;public class Server {public static void main(String[] args) {try (ServerSocket ss = new ServerSocket(9999)) {System.out.println("服務器已啟動,等待客戶端連接...");while (true) {Socket socket = ss.accept();String uuid = UUID.randomUUID().toString().substring(0, 4);try (OutputStream os = new FileOutputStream(uuid + ".png");InputStream is = socket.getInputStream();OutputStream sos = socket.getOutputStream()) {
// System.out.println( uuid + ".png");byte[] buffer = new byte[1024];int len;while ((len = is.read(buffer)) != -1) {System.out.println("Arrays.toString(buffer)");os.write(buffer, 0, len);}
// os.flush(); // 確保數據寫入sos.write("上傳成功".getBytes());
// System.out.println("文件接收完成:" + uuid + ".png");}socket.close();}} catch (Exception e) {e.printStackTrace();}}
}
Client
package homework;import java.io.*;
import java.net.Socket;public class client1 {public static void main(String[] args) throws IOException {String path = "D:\\30f.jpg";InputStream in = new FileInputStream(path);if (path.endsWith(".png") && in.available() < 1024 * 1024 * 2) {try (Socket s = new Socket("127.0.0.1", 9999);OutputStream os = s.getOutputStream();) {System.out.println("已連接到服務器");// 不能大于2mbbyte[] b = new byte[1024];int len;while ((len = in.read(b)) != -1) {os.write(b, 0, len);}s.shutdownOutput();
// s.shutdownOutput();InputStream is = s.getInputStream();byte[] bytes = new byte[1024];int len1;len1 = is.read(bytes);System.out.println(new String(bytes, 0, len1));} catch (Exception e) {e.printStackTrace();}} else {System.out.println("文件格式錯誤或文件大小要小于2M");}}
}
四.加密算法
1. 什么是編碼?
ASCII
碼就是一種編碼,字母A
的編碼是十六進制的0x41
,字母B
是0x42
,以此類推:
字母
A
B
C
D
…
?????????因為ASCII
編碼最多只能有127
個字符,要想對更多的文字進行編碼,就需要用占用2
個字節的Unicode
或者3
個字節的UTF-8
。例如:中文的"中"字使用Unicode
編碼就是0x4e2d
,UTF-8
編碼是0xe4b8ad
。
?
漢字 | Unicode編碼 | UTF-8編碼 |
中 | 0x4e2d | 0xe4b8ad |
文 | 0x6587 | 0xe69687 |
編 | 0x7f16 | 0xe7bc96 |
碼 | 0x7801 | 0xe7a081 |
2. Base64編碼
URL
編碼是對字符進行編碼,表示成%xx
的形式,而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
分別用字符+
和/
表示。
舉個例子:3
個byte
數據分別是e4
、b8
、ad
,按6bit
分組得到十六進制39
、0b
、22
和2d
,分別對應十進制57
、11
、34
、45
,通過索引計算結果為5Lit4
┌───────────────┬───────────────┬───────────────┐
│ e4 │ b8 │ ad │
└───────────────┴───────────────┴───────────────┘
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│1│1│1│0│0│1│0│0│1│0│1│1│1│0│0│0│1│0│1│0│1│1│0│1│二進制
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
┌───────────┬───────────┬───────────┬───────────┐
│ 39 │ 0b │ 22 │ 2d │十六進制
└───────────┴───────────┴───────────┴───────────┘
┌───────────┬───────────┬───────────┬───────────┐
│ 57 │ 11 │ 34 │ 45 │十進制
└───────────┴───────────┴───────────┴───────────┘
┌───────────┬───────────┬───────────┬───────────┐
│ 5 │ L │ i │ t │十進制
└───────────┴───────────┴───────────┴───────────┘
在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]}
}
因為標準的Base64
編碼會出現+
、/
和=
,所以不適合把Base64
編碼后的字符串放到URL
中。一種針對URL
的Base64
編碼可以在URL
中使用的Base64
編碼,它僅僅是把+
變成-
,/
變成_
:
public class Main {public static void main(String[] args) {// 原始字節內容byte[] input = new byte[] { 0x01, 0x02, 0x7f, 0x00 };// 分別使用兩種方式進行編碼String b64Encode = Base64.getEncoder().encodeToString(input);String b64UrlEncoded = Base64.getUrlEncoder().encodeToString(input);// 替換“+、/和=”System.out.println(b64Encode); System.out.println(b64UrlEncoded);// 分別使用兩種方式進行重新解碼byte[] output1 = Base64.getDecoder().decode(b64Encode);byte[] output2 = Base64.getUrlDecoder().decode(b64UrlEncoded);// 結果完全一致System.out.println(Arrays.toString(output1));System.out.println(Arrays.toString(output2));}
}
Base64
編碼的目的是把二進制數據變成文本格式,這樣在很多文本中就可以處理二進制數據。例如,電子郵件協議就是文本協議,如果要在電子郵件中添加一個二進制文件,就可以用Base64
編碼,然后以文本的形式傳送。
Base64
編碼的缺點是傳輸效率會降低,因為它把原始數據的長度增加了1/3。和URL
編碼一樣,Base64
編碼是一種編碼算法,不是加密算法。
如果把Base64
的64
個字符編碼表換成32
個、48
個或者58
個,就可以使用Base32
編碼,Base48
編碼和Base58
編碼。字符越少,編碼的效率就會越低。
3. 小結
URL
編碼和Base64
編碼都是編碼算法,它們不是加密算法;
URL
編碼的目的是把任意文本數據編碼為%
前綴表示的文本,便于瀏覽器和服務器處理;
Base64
編碼的目的是把任意二進制數據編碼為文本,但編碼后數據量會增加1/3.
五.哈希算法
1. 概述
哈希算法(Hash
)又稱摘要算法(Digest
),它的作用是:對任意一組輸入數據進行計算,得到一個固定長度的輸出摘要。
哈希算法最重要的特點就是:
- 相同的輸入一定得到相同的輸出;
- 不同的輸入大概率得到不同的輸出。
所以,哈希算法的目的:為了驗證原始數據是否被篡改。
Java
字符串的hashCode()
就是一個哈希算法,它的輸入是任意字符串,輸出是固定的4
字節int
整數:
"hello".hashCode(); // 0x5e918d2
"hello, java".hashCode(); // 0x7a9d88e8
"hello, bob".hashCode(); // 0xa0dbae2f
兩個相同的字符串永遠會計算出相同的hashCode
,否則基于hashCode
定位的HashMap
就無法正常工作。這也是為什么當我們自定義一個class
時,覆寫equals()
方法時我們必須正確覆寫hashCode()
方法。
2. 哈希碰撞
哈希碰撞是指:兩個不同的輸入得到了相同的輸出。
例如:
"AaAaAa".hashCode(); // 0x7460e8c0
"BBAaBB".hashCode(); // 0x7460e8c0"通話".hashCode(); // 0x11ff03
"重地".hashCode(); // 0x11ff03
碰撞能不能避免?答案是不能。碰撞是一定會出現的,因為輸出的字節長度是固定的,String
的hashCode()
輸出是4
字節整數,最多只有4294967296
種輸出,但輸入的數據長度是不固定的,有無數種輸入。所以,哈希算法是把一個無限的輸入集合映射到一個有限的輸出集合,必然會產生碰撞。
碰撞不可怕,我們擔心的不是碰撞,而是碰撞的概率,因為碰撞概率的高低關系到哈希算法的安全性。一個安全的哈希算法必須滿足:
- 碰撞概率低;
- 不能猜測輸出。
不能猜測輸出是指:輸入的任意一個bit
的變化會造成輸出完全不同,這樣就很難從輸出反推輸入(只能依靠暴力窮舉)。
假設一種哈希算法有如下規律:
hashA("java001") = "123456"
hashA("java002") = "123457"
hashA("java003") = "123458"
那么很容易從輸出123459
反推輸入,這種哈希算法就不安全。安全的哈希算法從輸出是看不出任何規律的:
hashB("java001") = "123456"
hashB("java002") = "580271"
hashB("java003") = ???
3. 常用哈希算法
哈希算法,根據碰撞概率,哈希算法的輸出長度越長,就越難產生碰撞,也就越安全。
常用的哈希算法有:
算法 | 輸出長度(位) | 輸出長度(字節) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Java
標準庫提供了常用的哈希算法,通過統一的接口進行調用。以MD5
算法為例,看看如何對輸入內容計算哈希:
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());}
}
使用MessageDigest
時,我們首先根據哈希算法獲取一個MessageDigest
實例,然后,反復調用update(byte[])
輸入數據。當輸入結束后,調用digest()
方法獲得byte[]
數組表示的摘要,最后,把它轉換為十六進制的字符串。
運行上述代碼,可以得到輸入HelloWorld
的MD5
是68e109f0f40ca72a15e05cc22786f8e6
。
4. 哈希算法的用途
4.1. 校驗下載文件
因為相同的輸入永遠會得到相同的輸出,因此,如果輸入被修改了,得到的輸出就會不同。我們在網站上下載軟件的時候,經常看到下載頁顯示的MD5哈希值:
如何判斷下載到本地的軟件是原始的、未經篡改的文件?我們只需要自己計算一下本地文件的哈希值,再與官網公開的哈希值對比,如果相同,說明文件下載正確,否則,說明文件已被篡改。
4.2. 存儲用戶密碼
哈希算法的另一個重要用途是存儲用戶口令。如果直接將用戶的原始口令存放到數據庫中,會產生極大的安全風險:
- 數據庫管理員能夠看到用戶明文口令;
- 數據庫數據一旦泄漏,黑客即可獲取用戶明文口令。
username | password |
bob | 123456789 |
alice | sdfsdfsdf |
tim | justdoit |
不存儲用戶的原始口令,那么如何對用戶進行認證?方法是存儲用戶口令的哈希,例如,MD5
。在用戶輸入原始口令后,系統計算用戶輸入的原始口令的MD5
并與數據庫存儲的MD5
對比,如果一致,說明口令正確,否則,口令錯誤。
因此,數據庫存儲用戶名和口令的表內容應該像下面這樣:
username | password |
bob | 25f9e794323b453885f5181f1b624d0b |
alice | 73a90acaae2b1ccc0e969709665bc62f |
tim | 19f9f30bd097d4c066d758fb01b75032 |
這樣一來,數據庫管理員看不到用戶的原始口令。即使數據庫泄漏,黑客也無法拿到用戶的原始口令。想要拿到用戶的原始口令,必須用暴力窮舉的方法,一個口令一個口令地試,直到某個口令計算的MD5
恰好等于指定值。
使用哈希口令時,還要注意防止彩虹表攻擊。
什么是彩虹表呢?上面講到了,如果只拿到MD5
,從MD5
反推明文口令,只能使用暴力窮舉的方法。然而黑客并不笨,暴力窮舉會消耗大量的算力和時間。但是,如果有一個預先計算好的常用口令和它們的MD5
的對照表,這個表就是彩虹表。如果用戶使用了常用口令,黑客從MD5
一下就能反查到原始口令:
常用口令 | MD5 |
hello123 | f30aa7a662c728b7407c54ae6bfd27d1 |
12345678 | 25d55ad283aa400af464c76d713c07ad |
passw0rd | bed128365216c019988915ed3add75fb |
19700101 | 570da6d5277a646f6552b8832012f5dc |
… | … |
wbjxxmy | 11d7a82f45f6a176fd9d5c100ccab40a |
這就是為什么不要使用常用密碼,以及不要使用生日作為密碼的原因。
當然,我們也可以采取特殊措施來抵御彩虹表攻擊:對每個口令額外添加隨機數,這個方法稱之為加鹽(salt
):
digest = md5(salt + inputPassword)
經過加鹽處理的數據庫表,內容如下:
username | salt | password |
bob | H1r0a | a5022319ff4c56955e22a74abcc2c210 |
alice | 7$p2w | e5de688c99e961ed6e560b972dab8b6a |
tim | z5Sk9 | 1eee304b92dc0d105904e7ab58fd2f64 |
5. 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());}
}
類似的,計算SHA-256
,我們需要傳入名稱"SHA-256
",計算SHA-512
,我們需要傳入名稱"SHA-512
"。Java
標準庫支持的所有哈希算法可以在這里查到。
6. 小結
- 哈希算法可用于驗證數據完整性,具有防篡改檢測的功能;
- 常用的哈希算法有
MD5
、SHA-1
等; - 用哈希存儲口令時要考慮彩虹表攻擊。
例題:
從本地獲取一張圖片,對這個圖片進行md加密
package jiamisuanfa;import java.io.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.UUID;public class Demo06 {public static void main(String[] args) throws IOException, NoSuchAlgorithmException {//題目:// 從本地獲取一張圖片,對這個圖片進行md加密InputStream is = new FileInputStream(new File("D:\\30f.jpg"));MessageDigest md = MessageDigest.getInstance("MD5");byte[] message = new byte[1024];String salt = UUID.randomUUID().toString().substring(0,6);while (is.read(message) != -1) {md.update(message);}byte[] bytes1 = md.digest();System.out.println("沒有加顏值的字符數組:"+ Arrays.toString(bytes1));System.out.println("沒有顏值加密后的字符串:"+ byteToHex(bytes1));md.update(salt.getBytes());byte[] bytes = md.digest();
// System.out.println("未加密:"+ Arrays.toString(message));System.out.println("加了顏值后的字符數組:"+ Arrays.toString(bytes));System.out.println("有了顏值加密后的字符串:"+ byteToHex(bytes));}public static String byteToHex(byte[] bytes){StringBuilder sb = new StringBuilder();for (byte b:bytes){sb.append(String.format("%02x",b));}return sb.toString();}
}
擴展:添加鹽值解決彩虹表攻擊
package jiamisuanfa;import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;public class Demo05 {// 此次的鹽值信息為:f2c01d// 加密后的結果為:[-60,35,-131,2,-124,1,1,1......]// 加密后的字符串為:c456888es6w6778ewr2// 加密后長度為:..// 添加鹽值解決彩虹表攻擊public static void main(String[] args) throws NoSuchAlgorithmException {// 1,創建MessageDigest實例MessageDigest md = MessageDigest.getInstance("MD5");// 2.使用md5進行信息加密byte[] message = "我本將心像明月".getBytes();md.update(message);// 3,添加鹽值,隨機值// String uuid = UUID.randomUUID().toString().substring(0,6);// System.out.println("此次的鹽值信息為:"+uuid);// md.update(uuid.getBytes());md.update("f2c01d".getBytes());// 3.進行加密byte[] bytes = md.digest();System.out.println("加密后的字節數組:"+ Arrays.toString(bytes));System.out.println("加密后的字符串:"+ byteToHex(bytes));System.out.println("加密后的字符串長度:"+ byteToHex(bytes).length());}public static String byteToHex(byte[] bytes){StringBuilder sb = new StringBuilder();for (byte b:bytes){sb.append(String.format("%02x",b));}return sb.toString();}
}
RipeMD160,SHA-1,MD5 創建,統一封裝成工具類:
package jiamisuanfa;import org.bouncycastle.jce.provider.BouncyCastleProvider;import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.Arrays;
import java.util.UUID;public class HashTools {// 創建信息摘要對象-成員變量private static MessageDigest md;private HashTools() {}// md5public static String md5(String message) throws NoSuchAlgorithmException {// 創建信息摘要對象md = MessageDigest.getInstance("MD5");return handle(message);}// sha-1public static String sha1(String message) throws NoSuchAlgorithmException {md = MessageDigest.getInstance("SHA-1");return handle(message);}public static String ripeMD168(String message) throws NoSuchAlgorithmException {// 安全注冊中心// 注冊BouncyCastle提供的通知類對象BouncyCastleProviderSecurity.addProvider(new BouncyCastleProvider());md = MessageDigest.getInstance("RipeMD160");return handle(message);}private static String handle(String message) {byte[] bytes = message.getBytes();md.update(bytes);// 添加顏值md.update(UUID.randomUUID().toString().substring(0,6).getBytes());// 加密操作byte[] bytes1 = md.digest();// 轉字符串return byteToHex(bytes1);}public static String byteToHex(byte[] bytes){StringBuilder sb = new StringBuilder();for (byte b:bytes){// 將每一個字節數字轉成2位的16進制的字符串數字sb.append(String.format("%02x",b));}return sb.toString();}public static byte[] stringTobytes(String message){byte[] bytes = new byte[message.length()/2];for (int i = 0; i < message.length(); i=i+2){String subString = message.substring(i,i+2);byte b = (byte) Integer.parseInt(subString,16);bytes[i/2] = b;}return bytes;}}
?
package jiamisuanfa;import java.security.NoSuchAlgorithmException;public class Demo07 {public static void main(String[] args) throws NoSuchAlgorithmException {String message = "我本將心趙明月";String md5 = HashTools.md5(message);System.out.println("md5加密后的字符串:"+md5);System.out.println(md5.length());// 16*2String sha1 = HashTools.sha1(message);System.out.println("sha1加密后的字符串:"+sha1);String ripeMD168 = HashTools.ripeMD168(message);System.out.println("ripeMD160加密后的字符串:"+ripeMD168);System.out.println(ripeMD168.length());// 20*2}
}
六.Hmac算法
1. 概述
在前面講到哈希算法時,我們說,存儲用戶的哈希口令時,要加鹽存儲,目的就在于抵御彩虹表攻擊。我們回顧一下哈希算法:digest = hash(input)
正是因為相同的輸入會產生相同的輸出,我們加鹽的目的就在于,使得輸入有所變化:
digest = hash(salt + input)
這個salt
可以看作是一個額外的“認證碼”,同樣的輸入,不同的認證碼,會產生不同的輸出。因此,要驗證輸出的哈希,必須同時提供“認證碼”。
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
。
下面是使用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);}
}
和MD5
相比,使用HmacMD5
的步驟是:
- 通過名稱
HmacMD5
獲取KeyGenerator
實例; - 通過
KeyGenerator
創建一個SecretKey
實例; - 通過名稱
HmacMD5
獲取Mac
實例; - 用
SecretKey
初始化Mac實例; - 對
Mac
實例反復調用update(byte[])
輸入數據; - 調用
Mac
實例的doFinal()
獲取最終的哈希值。
我們可以用Hmac
算法取代原有的自定義的加鹽算法,因此,存儲用戶名和口令的數據庫結構如下:
?
username | secret_key (64 bytes) | password |
bob | a8c06e05f92e...5e16 | 7e0387872a57c85ef6dddbaa12f376de |
alice | e6a343693985...f4be | c1f929ac2552642b302e739bc0cdbaac |
tim | f27a973dfdc0...6003 | af57651c3a8a73303515804d4af43790 |
?有了Hmac
計算的哈希和SecretKey
,我們想要驗證怎么辦?這時,SecretKey
不能從KeyGenerator
生成,而是從一個byte[]
數組恢復:
// 原始密碼
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);
2. 小結
Hmac
算法是一種標準的基于密鑰的哈希算法,可以配合MD5
、SHA-1
等哈希算法,計算的摘要長度和原摘要算法長度相同。
案例:
登錄加密,和二次登錄,獲取key驗證
package jiamisuanfa;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Demo08 {/*密鑰字節信息:[19, -3, 97, -104, -71, 103, -63, -72, -81, -62, 124, -104, -93, -19, -36, -53, -60, -88, -128, 23, 60, 99, -90, 74, -57, -81, 21, 56, -68, 110, 127, -95, -51, -21, -96, -85, -72, -23, -118, -49, -50, 122, -71, 52, -49, 94, 6, -89, 14, 46, -11, -107, 127, 115, 90, 54, -90, -98, -96, -60, 41, -83, -81, 6]
密鑰字符串:13fd6198b967c1b8afc27c98a3eddccbc4a880173c63a64ac7af1538bc6e7fa1cdeba0abb8e98acfce7ab934cf5e06a70e2ef5957f735a36a69ea0c429adaf06
密鑰字節信息長度:64
加密后的字節數組:[-24, 18, -16, 11, -83, -19, 35, 50, 4, 104, 109, 51, -91, -43, 16, -64]
加密后的字符串:e812f00baded233204686d33a5d510c0
加密后的字符串長度:16*/public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException {
// // 產生密鑰生成器對象
// KeyGenerator generator = KeyGenerator.getInstance("HmacMD5");
// // 通過密鑰生成器生成密鑰
// SecretKey key = generator.generateKey();
// // 獲取密鑰字節信息
// byte[] keyBytes = key.getEncoded();
// System.out.println("密鑰字節信息:"+ Arrays.toString(keyBytes));
// System.out.println("密鑰字符串:"+HashTools.byteToHex(keyBytes));
// System.out.println("密鑰字節信息長度:"+ keyBytes.length);
// // 獲取加密對象
// Mac mas = Mac.getInstance("HmacMD5");
// // 初始化key值
// mas.init(key);
// // 提供需要進行加密的信息添加進來
// mas.update("我本將心像明月".getBytes());
// // 加密操作
// byte[] bytes = mas.doFinal();
// System.out.println("加密后的字節數組:"+ Arrays.toString(bytes));
// System.out.println("加密后的字符串:"+ HashTools.byteToHex(bytes));
// System.out.println("加密后的字符串長度:"+ bytes.length);// 登錄// 二次登錄// 密鑰字符串轉字節數組操作String str = "13fd6198b967c1b8afc27c98a3eddccbc4a880173c63a64ac7af1538bc6e7fa1cdeba0abb8e98acfce7ab934cf5e06a70e2ef5957f735a36a69ea0c429adaf06";byte[] keyBytes = HashTools.stringTobytes(str);System.out.println("密鑰字節信息:"+ Arrays.toString(keyBytes));// 2.還原密鑰keySecretKey key = new SecretKeySpec(keyBytes, "HmacMD5");// 再次加密Mac mas = Mac.getInstance("HmacMD5");// 初始化key值mas.init(key);// 提供需要進行加密的信息添加進來mas.update("我本將心像明月".getBytes());// 加密操作byte[] bytes = mas.doFinal();System.out.println("加密后的字節數組:"+ Arrays.toString(bytes));System.out.println("加密后的字符串:"+ HashTools.byteToHex(bytes));System.out.println("加密后的字符串長度:"+ bytes.length);}
}
七.對稱加密算法
1. 概述
對稱加密算法就是傳統的用一個秘鑰進行加密和解密。例如,我們常用的WinZIP
和WinRAR
對壓縮包的加密和解密,就是使用對稱加密算法.
算法 | 密鑰長度 | 工作模式 | 填充模式 |
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
標準庫提供的算法實現并不包括所有的工作模式和所有填充模式。
最后,值得注意的是,DES
算法由于密鑰過短,可以在短時間內被暴力破解,所以現在已經不安全了。
2. 使用AES加密
AES
算法是目前應用最廣泛的加密算法。比較常見的工作模式是ECB
和CBC
。
2.1. ECB模式
ECB
模式是最簡單的AES
加密模式,它需要一個固定長度的密鑰,固定的明文會生成固定的密文。
我們先用ECB
模式加密并解密:
package jiamisuanfa;import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;public class Demo09 {public static void main(String[] args) throws IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException {// 加密byte[] input = "我本將心像明月".getBytes();byte[] keys = "1234567890abcdef".getBytes();byte[] encodeBytes = encodeMessage(input, keys);System.out.println("加密后的字符串:"+ HashTools.byteToHex(encodeBytes));System.out.println("加密后的字符串長度:"+ encodeBytes.length);System.out.println("加密后的結果:"+ Arrays.toString(encodeBytes));// 解密byte[] decodeBytes = decodeMessage(encodeBytes, keys);System.out.println("解密后的字符串:"+ new String(decodeBytes));System.out.println("解密后的字符串長度:"+ decodeBytes.length);System.out.println("解密后的結果:"+ Arrays.toString(decodeBytes));}public static byte[] encodeMessage(byte[] input, byte[] keys) throws IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {// 1.讀取對稱加密對象,設置加密算法,工作模式,填充模式Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");// 2.加密準備密鑰信息SecretKeySpec key = new SecretKeySpec(keys, "AES");// 3.初始化加密對象cipher.init(Cipher.ENCRYPT_MODE, key);// 4.執行加密操作byte[] bytes = cipher.doFinal(input);return bytes;}public static byte[] decodeMessage(byte[] input, byte[] keys) throws IllegalBlockSizeException, BadPaddingException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException {// 1.讀取對稱加密對象,設置加密算法,工作模式,填充模式Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");// 2.加密準備密鑰信息SecretKeySpec key = new SecretKeySpec(keys, "AES");// 3.初始化加密對象cipher.init(Cipher.DECRYPT_MODE, key);// 4.執行加密操作byte[] bytes = cipher.doFinal(input);return bytes;}
}
?
Java
標準庫提供的對稱加密接口非常簡單,使用時按以下步驟編寫代碼:
- 根據算法名稱/工作模式/填充模式獲取
Cipher
實例; - 根據算法名稱初始化一個
SecretKey
實例,密鑰必須是指定長度; - 使用
SerectKey
初始化Cipher
實例,并設置加密或解密模式; - 傳入明文或密文,獲得密文或明文。
2.2. CBC模式
ECB
模式是最簡單的AES
加密模式,這種一對一的加密方式會導致安全性降低。所以,更好的方式是通過CBC
模式,它需要一個隨機數作為IV
參數,這樣對于同一份明文,每次生成的密文都不同:
package jiamisuanfa;import javax.crypto.*;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;public class Demo11 {public static void main(String[] args) throws InvalidAlgorithmParameterException, IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException {byte[] input = "我本將心像明月".getBytes();byte[] keys = "1234567890ebadfj".getBytes();byte[] encode = encodeMessage(input, keys);System.out.println("加密后的信息為:"+ Arrays.toString( encode));byte[] decode = decodeMessage(encode, keys);System.out.println("解密后的信息為:"+ new String(decode));}private static byte[] encodeMessage(byte[] message,byte[] key) throws IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException {// 1.獲取Cipher對象Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");// 2.還原keySecretKey keys = new SecretKeySpec(key, "AES");// 3.準備iv偏移SecureRandom sr = SecureRandom.getInstanceStrong();byte[] bytes = sr.generateSeed(16);IvParameterSpec iv = new IvParameterSpec(bytes);// 4.初始化操作,設置加密模式,設置key,設置偏移量cipher.init(Cipher.ENCRYPT_MODE, keys, iv);// 5.加密byte[] encode = cipher.doFinal(message);return addByte(encode, bytes);}private static byte[] addByte(byte[] encode, byte[] iv) {byte[] bytes = new byte[encode.length+iv.length];System.arraycopy(encode,0,bytes,0,encode.length);System.arraycopy(iv,0,bytes,encode.length,iv.length);return bytes;}private static byte[] decodeMessage(byte[] decodeMessage,byte[] key) throws InvalidAlgorithmParameterException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException {// 1.獲取Cipher對象Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");// 2.還原keySecretKey keys = new SecretKeySpec(key, "AES");// 3.還原iv值byte[] bytes = Arrays.copyOfRange(decodeMessage, decodeMessage.length-16, decodeMessage.length);IvParameterSpec iv = new IvParameterSpec(bytes);// 4.初始化操作,設置加密模式,設置key,設置偏移量cipher.init(Cipher.DECRYPT_MODE, keys, iv);// 5.解密byte[] bytes1 = Arrays.copyOf(decodeMessage, decodeMessage.length-16);return bytes1;}}
3. 小結
- 對稱加密算法使用同一個密鑰進行加密和解密,常用算法有
DES
、AES
和IDEA
等; - 密鑰長度由算法設計決定,
AES
的密鑰長度是128
/192
/256
位; - 使用對稱加密算法需要指定算法名稱、工作模式和填充模式。