# Java 中的 Hashing ## 1. 什麼是 Hashing? ### **定義與概念** Hashing 是一種將輸入數據透過特定的演算法轉換為固定長度的值(通常稱為 Hash 值或雜湊值)的技術。這些 Hash 值用於快速查找、驗證數據完整性,或作為唯一標識符。 ### **為何需要 Hashing?** Hashing 在計算機科學中被廣泛應用,主要有以下幾個原因: - **高效搜尋**:透過 Hashing,可以快速存取資料,例如在 `HashMap` 或 `HashSet` 中進行 O(1) 時間複雜度的查找。 > **O(1) 時間複雜度(常數時間)**:表示操作的執行時間與輸入大小無關,無論數據有多少,每次查找或插入的時間幾乎不變。例如,在 HashMap 中,透過 Hashing 直接定位數據位置,因此查找通常是 O(1)。 > **O(n) 時間複雜度(線性時間)**:表示操作的執行時間與輸入大小成正比,當數據量增加時,操作時間也會線性增加。例如,在 List 中,查找操作通常是 O(n)。 > **HashMap 的運作原理**:HashMap 使用 Hashing 將 Key 映射到一個固定範圍的索引,這使得在 HashMap 中查找 Key 的時間複雜度為 O(1)。 > **HashSet 的運作原理**:HashSet 內部實際上是一個 HashMap,只是將 Key 與 Value 都設置為同一個對象,並將 Value 設置為一個固定的常數。這樣,HashSet 就可以利用 HashMap 的 Hashing 來快速查找對象。 - **數據完整性驗證**:因為 Hashing 是單向的,即從 Hash 值想要推算出原始數據幾乎不可能,因此若是想要通過修改數據來得到相同的 Hash 值,這是非常困難的。這使得 Hashing 可以用於驗證數據完整性,確保數據未被篡改。 - **加密與安全性**:同前一點,Hashing 也被廣泛應用於密碼存儲、數據加密等安全性相關的場景。大多數使用的 Hash 演算法是使用 MD5、SHA-1、SHA-256 等。只要有一個字元的變化,Hash 值就會完全不同。 - **唯一性與識別**:從理論上來說,Hashing 的結果是唯一的,即不同的輸入對應不同的 Hash 值。且使用同一個 Hash 函數。所產出的值的長度是固定的,使得存儲與比對更加高校與方便,很適合作為唯一標識符。如 UUID(Universally Unique Identifier)就是一種基於 Hashing 的唯一標識符。 ### **Hashing 的應用** Hashing 在許多領域中都有廣泛的應用,包括但不限於: - **集合框架(Java Collections Framework)** - HashMap<K, V>:透過 hashCode() 來決定鍵值對的存儲位置,以提供 O(1) 的查找速度。 - HashSet<E>:基於 HashMap,使用 Hashing 來確保元素的唯一性,避免重複數據。 - Hashtable<K, V>:類似 HashMap,但是同步的(Thread-safe)。 - **字串 Hashing** - String.hashCode():用來計算字串的 Hash 值,可用於快速比對字串或作為鍵值對的鍵。 - **物件的唯一性判斷** - hashCode() 和 equals() 方法:在集合中使用 Hashing 來確保物件的唯一性,例如存入 HashSet 或作為 HashMap 的鍵。 - **文件完整性驗證** - MessageDigest(MD5, SHA-256 等):用來計算文件的 Hash 值,以驗證文件是否被篡改。 - **分佈式系統** - 一致性 Hashing:在分佈式系統(如負載均衡、NoSQL 資料庫)中,透過 Hashing 來分配請求至不同的節點。這個部分的概念好像有點難懂。之後再寫一篇來說明。不過主要的概念就是傳統的 Hashing(如 hash(key) % N)在節點數變動時,可能會導致大量數據重新分佈,影響系統效能。而一致性 Hashing 透過 Hash 環(Hash Ring)解決這個問題,使得節點的增減對數據分佈的影響降到最低。 - **密碼存儲與加密** - PBKDF2, BCrypt, Argon2:透過 Hashing 加鹽(Salting)來保護密碼。 - **UUID(Universally Unique Identifier)** - UUID.randomUUID().toString():基於 Hashing 來生成全球唯一的識別碼。 - **緩存與快取機制** - ConcurrentHashMap:基於 Hashing 的多執行緒安全的快取資料結構,避免競爭條件(Race Condition)。 - **數據去重** - 在大數據應用中,透過 Hashing 來檢查數據是否重複,例如利用 Bloom Filter 來快速判斷數據是否已經出現過。 ## 2. Java 中的 Hash 相關 API - **`hashCode()` 方法** - 永來返回物件的 Hash code,用於確保物件的唯一性。 - 主要是用在集合框架中,如 HashMap、HashSet。 - 預設的 `hashCode()` 方法是根據物件的記憶體地址計算的,不同物件的 `hashCode()` 通常不同。而通常我們會需要覆寫 `hashCode()` 方法,以確保物件的唯一性。 - 範例: ```java class Person { String name; int age; @Override public int hashCode() { // 通常我們會使用這個物件中代表唯一性的幾個屬性來計算 hashCode,這樣就可以更加減少碰撞。 return Objects.hash(name, age); } } public class HashCodeExample { public static void main(String[] args) { Person p1 = new Person(); p1.name = "Alice"; p1.age = 25; System.out.println("Hash Code: " + p1.hashCode()); } } - **`MessageDigest` 類** - MessageDigest 用於產生加密 Hash,如 SHA-256、MD5。 - 通常用於驗證文件完整性、密碼加密等場景。 - 加密過後的 Hash 是不可逆的,即無法從 Hash 值反推出原始數據。 - 也可以當作數位簽章,確保文件的完整性。 - 範例: ```java import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; public class MessageDigestExample { public static void main(String[] args) throws NoSuchAlgorithmException { String input = "HelloWorld"; MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] hash = md.digest(input.getBytes()); System.out.println("SHA-256 Hash: " + Arrays.toString(hash)); } } ``` - **`SecureRandom` 類** - SecureRandom 用於生成安全的隨機數。 - 通常用於密碼學、安全性相關的場景。 - SecureRandom 是 Java 安全性提供的隨機數生成器,比 `Random` 類更安全。 - 範例: ```java import java.security.SecureRandom; public class SecureRandomExample { public static void main(String[] args) { SecureRandom random = new SecureRandom(); byte[] bytes = new byte[16]; random.nextBytes(bytes); System.out.println("Secure Random: " + Arrays.toString(bytes)); } } ``` - **`Mac` 類** - Mac 用於生成 HMAC(Hash-based Message Authentication Code)。 - HMAC 是一種基於 Hash 的訊息驗證碼,用於驗證數據的完整性和真實性。 - 通常用於數據通信、網路安全等場景。 - 範例: ```java import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Arrays; public class MacExample { public static void main(String[] args) throws NoSuchAlgorithmException, InvalidKeyException { String input = "HelloWorld"; String key = "SecretKey"; Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(key.getBytes(), "HmacSHA256")); byte[] hash = mac.doFinal(input.getBytes()); System.out.println("HMAC-SHA256 Hash: " + Arrays.toString(hash)); } } ``` - **`UUID` 類** - UUID 用於生成全球唯一的識別碼。 - UUID 是一種 128 位的數字,通常用於唯一標識符。 - Java 中的 `UUID.randomUUID()` 可以生成隨機的 UUID。 - 範例: ```java import java.util.UUID; public class UUIDExample { public static void main(String[] args) { UUID uuid = UUID.randomUUID(); System.out.println("UUID: " + uuid); } } ``` ## 3. `hashCode()` 與 `equals()` 方法 - **什麼是 `hashCode()`?** - `hashCode()` 方法用於計算物件的 Hash 值。 - Hash 值是一個整數,用於確保物件的唯一性。 - **什麼是 `equals()`?** - `equals()` 方法用於比較兩個物件是否相等。 - 而 `==` 用於比較兩個物件的內存地址。 - 範例: ```java public class EqualsExample { public static void main(String[] args) { String str1 = new String("Hello"); String str2 = new String("Hello"); // 比較內存地址 System.out.println("Using == : " + (str1 == str2)); // false // 比較內容 System.out.println("Using equals() : " + str1.equals(str2)); // true } } ``` - `hashCode()` 與 `equals()` 的關係** - 在 Java 中,如果兩個物件相等(`equals()` 返回 `true`),則它們的 `hashCode()` **必須相等**。 - 這是因為在 Hash 資料結構(如 `HashMap`、`HashSet`)中: - `hashCode()` 決定物件應該存放在哪個 **桶(bucket)** 中。 - `equals()` 用於進一步檢查桶內的物件是否相等(處理 Hash 碰撞)。 - **重要規則**: - **如果 `equals()` 返回 `true`,則 `hashCode()` 必須相等**。 - **但如果 `hashCode()` 相同,`equals()` 不一定為 `true`(因為可能發生 Hash 碰撞)。** - 為什麼覆寫 `equals()` 需要覆寫 `hashCode()`?** - Java 預設行為 - `Object` 類別的 `equals()` 預設是比較記憶體位址(`==`)。 - `Object` 的 `hashCode()` 會基於記憶體位址計算。 - 如果只覆寫 `equals()` 而不覆寫 `hashCode()`,會導致集合行為異常** - 例如 `HashSet` 會用 `hashCode()` 來決定物件的存放位置,若 `hashCode()` 不一致,則即使 `equals()` 相等,物件仍可能存入不同位置,導致集合內部有重複元素。 - 正確的做法 - **當覆寫 `equals()` 時,一定要覆寫 `hashCode()`,確保相等的物件有相同的 Hash 值,以保證在 `HashMap`、`HashSet` 等結構中的正確行為。** > 小提醒 > 在某些類別中,equals 被覆寫了,如 String 因為有 String pool 的緣故,使用 `equals()` 比較特別,要注意一下,這邊就不多說了 - 為什麼覆寫 `equals()` 需要覆寫 `hashCode()`? - 在預設情況下,`equals()` 方法是根據物件的內存地址來比較的,而 `hashCode()` 方法是根據物件的內存地址計算的。 - 因此,為了保證 `hashCode()` 和 `equals()` 的一致性,我們通常會覆寫這兩個方法。 - 範例: ```java import java.util.Objects; class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Person person = (Person) obj; return age == person.age && name.equals(person.name); } @Override public int hashCode() { return Objects.hash(name, age); } } public class Main { public static void main(String[] args) { Person p1 = new Person("Alice", 25); Person p2 = new Person("Alice", 25); System.out.println(p1.equals(p2)); // true,因為內容相同 System.out.println(p1.hashCode() == p2.hashCode()); // true,因為覆寫了 hashCode() // 放入 HashSet 測試 java.util.HashSet<Person> set = new java.util.HashSet<>(); set.add(p1); set.add(p2); // 不會被當作重複的物件(若未覆寫 hashCode() 則會出錯) System.out.println(set.size()); // 正確結果應為 1 } } ``` ## 4. Hashing 在 Java Collection Framework 中的應用 - **`HashMap` 如何利用 Hashing 進行資料存取?** - HashMap<K, V> 是 Java 中最常用的雜湊結構之一,主要用於 鍵值對(Key-Value) 的存取。其運作方式如下: - 將 Key 透過 `hashCode()` 計算出 Hash 值。 - 將 Hash 值映射到一個固定範圍的索引(桶)Bucket。 - 將 Key-Value 存入該索引中。 - 當查找時,再次計算 Key 的 Hash 值,找到對應的索引,取出 Value。 - 當插入時,若 Bucket 為空,直接存入;若 Bucket 已有元素,則進行碰撞處理(Chaining 或 Open Addressing)。 - 若鍵相同(equals() 判斷),則更新值。 - 若鍵不同,則使用 碰撞處理機制(例如 Chaining)。 - HashMap 會使用與插入相同的 hashCode() 和 index 計算方式來查找鍵值對,因此讀取的時間複雜度通常為 O(1),但當發生碰撞時,則可能增加到 O(n)。 - **`HashSet` 的運作方式** - HashSet<E> 是基於 HashMap 的 Set 實現,用於存儲唯一元素。 - HashSet 內部實際上是一個 HashMap,只是將 Key 和 Value 都設置為同一個對象,並將 Value 設置為一個固定的常數。 - HashSet 通過 Hashing 來確保元素的唯一性,避免重複數據。 - 當我們將元素存入 HashSet 時,HashSet 會將元素作為 Key 存入 HashMap,Value 則設置為一個固定的常數。 - 由於 HashSet 是透過 HashMap 來運作,因此其效能與 HashMap 相同,即大部分操作的時間複雜度為 O(1)。 - **`Hashtable` vs `HashMap` 的比較** | 特性 | HashMap | Hashtable | | ---- | ---- | ---- | | 執行緒安全性 | 非同步(非執行緒安全) | 同步(執行緒安全) | | 允許 null 值 | 允許 null 作為 Key 或 Value | 不允許 null 作為 Key 或 Value | | 效能 | 效能較高(無同步鎖) | 效能較低(每個方法都有同步鎖) | | 併發使用建議 | 建議使用 ConcurrentHashMap | 不建議使用,已過時 | - **碰撞處理技術**(Chaining、Open Addressing) - **Chaining 鏈結法**:當發生碰撞時,將相同 Hash 值的元素存入同一個桶中,形成一個鏈表。在 Java 8 之後,當鏈結串列的長度超過 8,並且陣列大小超過 64,則會轉換為 紅黑樹(Red-Black Tree) 來提升效能。 - 優點:簡單、易於實現。 - 缺點:當鏈表過長時,查找效率會下降,時間複雜度可能增加到 O(n)。 - 示意圖: ```mermaid graph TD; A["Index 0"] -->|null| X0 B["Index 1"] --> A1["A"] --> B1["B"] --> C1["C"] D["Index 2"] -->|null| X2 E["Index 3"] --> D1["D"] classDef empty fill:#f0f0f0,stroke:#ccc,stroke-width:1px; class X0,X2 empty; ``` - **Open Addressing 開放定址法**:當發生碰撞時,將元素存入其他空桶中,直到找到空桶為止。 - 優點:不需要額外的空間存儲鏈表。 - 缺點:容易產生聚集(Clustering)現象,導致查找效率下降。開放定址法可能會導致群聚現象(Clustering),影響查詢效率,因此現代 Java HashMap 採用 Chaining 為主要方式。 - 示意圖: ```mermaid graph TD; A["Index 0"] --> A1["A"] B["Index 1"] --> B1["B"] C["Index 2"] -->|Collision!| C1["B'"] D["Index 3"] --> D1["C"] C1 -.->|Probe Next| C2["B' (Moved)"] C2 -.->|Probe Next| C3["B' (Moved)"] ``` - **Rehashing**:當 HashMap 的負載因子(Load Factor)超過一定閾值時,會進行 Rehashing,即將 HashMap 的大小擴大一倍,並重新計算所有元素的 Hash 值。 - 負載因子 = 元素個數 / 桶的數量 - 通常負載因子為 0.75,即當元素個數超過桶數量的 75% 時,進行 Rehashing。 - Rehashing 的時間複雜度為 O(n),但由於發生的頻率較低,因此平均時間複雜度仍為 O(1)。 ## 5. Java 的 Hashing 演算法 - **`String.hashCode()` 的實作方式** - 在 Java 中,String 類別的 hashCode() 方法實作了一種簡單的 基於乘法的雜湊函數,其演算法如下: - 將字串的每個字元轉換為 Unicode 編碼(ASCII)。 - 進行乘法運算:`hash = 31 * hash + charAt(i)`。 - 最後返回 hash 值。 - 為什麼是 31? - 31 是一個質數,且 31 * x = (x << 5) - x,這樣可以用位運算來代替乘法,提高效能。 - 31 是一個奇數,乘法運算可以保證不會丟失任何訊息。 - 31 這個值在大多數情況下能夠分布較均勻,減少碰撞機率。 - **`Objects.hash()` 如何計算 Hash 值?** - Java 7 引入了 Objects.hash(),用來計算多個物件的 Hash 值,適用於 equals() 需要考慮多個欄位的類別。Objects.hash() 使用的是 變數長度參數(Varargs)。 - Objects.hash() 與 String.hashCode() 類似,但它可以接受多個變數,並使用 31 作為乘數來降低碰撞機率。 - **進階 Hashing(如 MurmurHash、Guava 的 `Hashing` API)** - MurmurHash 是一種非加密的雜湊函數,適用於高效搜尋和數據存取。相較於 String.hashCode(),MurmurHash 具有: - 更少的碰撞(Collision)。 - 更好的分布性(Uniform Distribution)。 - 比 CRC32 或 MD5 更快。 - Google Guava 提供了一系列強大的 Hash 演算法,例如: - Hashing.murmur3_128() - MurmurHash 128-bit - Hashing.sha256() - SHA-256 - Hashing.md5() - MD5 - Hashing.farmHashFingerprint64() - Google 的高效雜湊演算法 - 比較 | 方法 | 主要用途 | 優勢 | 可能的缺點 | |------|--------|-----|----------| | **`String.hashCode()`** | `String` 的 Hash 計算 | 快速,簡單,內建於 Java | 容易產生碰撞 | | **`Objects.hash()`** | 多個變數的 Hash 計算 | 方便計算複合鍵 | 效能稍慢(需額外創建陣列) | | **MurmurHash** | 高效 Hash,適用於大數據 | 分布均勻,低碰撞率 | 需要額外的依賴庫 | | **Guava `Hashing` API** | 可選 SHA256、Murmur、MD5 等 | 內建多種強大演算法 | 需要引入 Google Guava | ## 7. Hashing 的潛在問題與最佳實踐 - **Hash 碰撞(Collision)問題** - **低效能 Hash 函數的影響** - **如何選擇良好的 Hash 函數?** - **Hashing 安全性考量(防止 DoS 攻擊)** ## 8. Java 內建加密 Hashing(進階) - **`MessageDigest`(SHA-256, MD5)** - MD5 已經不安全,不建議使用。容易被彩虹表攻擊。 - 範例: ```java import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; public class SHA256Example { public static void main(String[] args) throws NoSuchAlgorithmException { String input = "hello"; MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(input.getBytes()); System.out.println(Arrays.toString(hash)); // 以 byte 陣列形式顯示雜湊值 } } ``` - **`SecureRandom` 生成安全隨機數** - SecureRandom 是 Java 內建的安全隨機數生成器,適用於 加密金鑰生成、密碼鹽(Salt) 等應用。 - **`Mac` 進行 HMAC 驗證** - Mac(Message Authentication Code)適合用於 API 簽名和 JWT 簽名驗證的原因在於它能夠提供數據的完整性和真實性驗證。Mac 使用一個密鑰來生成基於 Hash 的訊息驗證碼(HMAC),這樣可以確保數據在傳輸過程中未被篡改,並且只有持有密鑰的雙方才能生成和驗證簽名。因為以下的特性,所以是何作為雙方 API 之間的驗證。 - 完整性:HMAC 可以確保數據在傳輸過程中未被篡改。如果數據被修改,驗證時生成的 HMAC 將不匹配。 - 真實性:只有持有密鑰的雙方才能生成和驗證 HMAC,確保數據的來源是可信的。 - 安全性:HMAC 結合了 Hash 函數和密鑰,提供了比單純的 Hash 函數更高的安全性。 - 請求端生成 API 簽名範例: ```java import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Map; public class HmacClient { private static final String SECRET_KEY = "superSecretKey123"; public static String generateHmacSignature(String method, String path, Map<String, String> params) throws Exception { // 構建要簽名的數據(例如:請求方法 + 路徑 + 參數) StringBuilder data = new StringBuilder(); data.append(method).append(path); params.forEach((key, value) -> data.append(key).append("=").append(value).append("&")); // 使用 HMAC-SHA256 產生簽名 Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), "HmacSHA256"); mac.init(secretKey); byte[] hmacBytes = mac.doFinal(data.toString().getBytes()); return Base64.getEncoder().encodeToString(hmacBytes); // 轉為 Base64 字串 } public static void main(String[] args) throws Exception { String method = "GET"; String path = "/api/data"; Map<String, String> params = Map.of("user", "alice", "timestamp", "1710000000"); String signature = generateHmacSignature(method, path, params); System.out.println("Generated Signature: " + signature); } } ``` - 送出 Request,並把剛剛生成的簽名放在 Header 中,Server 端就可以透過相同的方法驗證簽名。 ```text GET /api/data?user=alice&timestamp=1710000000 Authorization: HMAC-SHA256 {signature} ``` - 接收端驗證 API 簽名範例: ```java import org.springframework.web.bind.annotation.*; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; import java.util.Map; @RestController @RequestMapping("/api") public class HmacServerController { private static final String SECRET_KEY = "superSecretKey123"; @GetMapping("/data") public String getData( @RequestParam String user, @RequestParam String timestamp, @RequestHeader("Authorization") String authHeader ) throws Exception { // 取得客戶端傳來的 HMAC 簽名 String receivedSignature = authHeader.replace("HMAC-SHA256 ", ""); // 重新計算 HMAC String recalculatedSignature = generateHmacSignature("GET", "/api/data", Map.of("user", user, "timestamp", timestamp)); // 驗證簽名是否正確 if (!receivedSignature.equals(recalculatedSignature)) { return "401 Unauthorized - Signature mismatch!"; } return "Hello " + user + ", your request is verified!"; } private String generateHmacSignature(String method, String path, Map<String, String> params) throws Exception { StringBuilder data = new StringBuilder(); data.append(method).append(path); params.forEach((key, value) -> data.append(key).append("=").append(value).append("&")); Mac mac = Mac.getInstance("HmacSHA256"); SecretKeySpec secretKey = new SecretKeySpec(SECRET_KEY.getBytes(), "HmacSHA256"); mac.init(secretKey); byte[] hmacBytes = mac.doFinal(data.toString().getBytes()); return Base64.getEncoder().encodeToString(hmacBytes); } } ``` ## 9. 通常我們都怎麼做會員系統的密碼加密? 1. 使用者輸入密碼 2. 系統產生一個隨機的「鹽」(Salt) 3. 將密碼與鹽結合後進行 Hashing 4. 將「鹽」和「Hash 後的密碼」存入資料庫 5. 使用者輸入密碼 6. 從資料庫查出「該使用者的鹽」及「存儲的 Hash 密碼」 7. 將「使用者輸入的密碼」與「鹽」組合後重新 Hash 8. 比較重新 Hash 的結果與資料庫中的 Hash 密碼 9. 若匹配,則登入成功;否則登入失敗 ```java import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.SecureRandom; import java.util.Base64; public class PasswordHashing { private static final int ITERATIONS = 10000; private static final int KEY_LENGTH = 256; public static String generateSalt() { SecureRandom random = new SecureRandom(); byte[] salt = new byte[16]; random.nextBytes(salt); return Base64.getEncoder().encodeToString(salt); } public static String hashPassword(String password, String salt) throws Exception { PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), Base64.getDecoder().decode(salt), ITERATIONS, KEY_LENGTH); SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); byte[] hash = factory.generateSecret(spec).getEncoded(); return Base64.getEncoder().encodeToString(hash); } public static void main(String[] args) throws Exception { String password = "MySecurePassword"; String salt = generateSalt(); String hashedPassword = hashPassword(password, salt); System.out.println("Salt: " + salt); System.out.println("Hashed Password: " + hashedPassword); } } ``` - 掌握幾個心法重點 - 不可逆(Irreversible) - Hashing 是 單向函數,意味著無法從 Hash 值反推出原始密碼。 - 這與加密(Encryption)不同,加密是可逆的(可以用密鑰解密),而 Hashing 不能解密。 - 相同輸入 → 相同輸出 - 如果輸入的密碼和鹽相同,則 Hash 值一定相同。 - 不同輸入,即使只改變一個字元,Hash 值也會完全不同(雪崩效應)。 ###### tags: `Java` `Hashing` `Security` `Encryption` `Hash` `Salt` `PBKDF2` `SecureRandom` `MessageDigest` `Mac` `UUID`