# 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×tamp=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`