文章目錄
- 第一部分:PBKDF2基礎概念
- 1. 什么是PBKDF2?
- 2. 為什么需要PBKDF2?
- 3. PBKDF2的工作原理
- 4. PBKDF2與其他密碼散列函數的比較
- 第二部分:在Java和SpringBoot中使用PBKDF2
- 1. Java內置的PBKDF2支持
- 2. SpringBoot中集成PBKDF2
- 2.1 添加依賴
- 2.2 配置PBKDF2密碼編碼器
- 2.3 自定義PBKDF2編碼器參數
- 2.4 在用戶服務中使用PBKDF2編碼器
- 第三部分:PBKDF2實戰 - 完整SpringBoot應用示例
- 1. 項目結構
- 2. Maven依賴
- 3. 應用屬性配置
- 4. 用戶模型
- 5. 用戶存儲庫
- 6. 安全配置
- 7. 用戶詳情服務
- 8. 用戶服務
- 9. 控制器
- 10. 模板頁面
- 11. 應用主類
- 第四部分:PBKDF2性能和安全性分析
- 1. 如何選擇適當的迭代次數
- 2. 鹽值長度選擇
- 3. PBKDF2與Bcrypt/Argon2的比較
- 第五部分:PBKDF2最佳實踐和常見錯誤
- 1. PBKDF2安全最佳實踐
- 2. 常見錯誤
- 錯誤1:使用固定或可預測的鹽值
- 錯誤2:迭代次數過低
- 錯誤3:在客戶端進行密碼散列
- 錯誤4:忽略密碼長度限制
- 錯誤5:不正確地比較散列值
- 3. 在生產環境中的注意事項
- 第六部分:總結
- 1. PBKDF2要點總結
- 錯誤3:在客戶端進行密碼散列
- 錯誤4:忽略密碼長度限制
- 錯誤5:不正確地比較散列值
- 3. 在生產環境中的注意事項
- 第六部分:總結
- 1. PBKDF2要點總結
第一部分:PBKDF2基礎概念
1. 什么是PBKDF2?
PBKDF2(Password-Based Key Derivation Function 2)是一種密鑰派生函數,由RSA實驗室的RSA公共密鑰加密標準(PKCS)系列中的第5號文檔《基于密碼的加密標準》(PKCS#5 v2.0)定義,并已成為Internet工程任務組(IETF)的RFC 2898標準。
簡單來說,PBKDF2是一種用于將用戶密碼安全地轉換為加密密鑰或散列值的算法,它專門設計用來抵抗暴力破解和字典攻擊。
2. 為什么需要PBKDF2?
在現代應用程序中,我們不應該以明文形式存儲用戶密碼,而應該存儲密碼的散列值。傳統的散列函數(如MD5或SHA-1)存在一些問題:
- 速度太快:現代硬件可以快速計算這些散列值,使暴力破解成為可能
- 缺乏鹽值(salt):相同的輸入總是產生相同的輸出,這使預計算攻擊(彩虹表)成為可能
- 并行計算:攻擊者可以使用GPU并行計算多個散列值
PBKDF2解決了這些問題:
- 它引入了計算成本因子(迭代次數),使計算過程變慢
- 它使用隨機鹽值,確保即使相同的密碼也會生成不同的散列值
- 它是順序計算的,難以并行化,這降低了GPU加速攻擊的效率
3. PBKDF2的工作原理
詳細步驟解析:
- 輸入收集:獲取用戶密碼和隨機生成的鹽值
- 偽隨機函數選擇:通常使用HMAC-SHA1、HMAC-SHA256或HMAC-SHA512
- 應用PBKDF2函數:
- 將密碼和鹽值作為輸入
- 指定迭代次數(通常為至少10,000次,現代系統建議100,000次以上)
- 指定所需的輸出長度(通常為256位或512位)
- 輸出:生成最終的密鑰或散列值
數學上,PBKDF2可表示為:
DK = PBKDF2(PRF, Password, Salt, c, dkLen)
其中:
- DK:派生密鑰
- PRF:偽隨機函數(如HMAC-SHA256)
- Password:用戶密碼
- Salt:隨機鹽值
- c:迭代次數
- dkLen:派生密鑰的長度
4. PBKDF2與其他密碼散列函數的比較
函數 | 優點 | 缺點 | 安全性 |
---|---|---|---|
MD5/SHA | 速度快 | 無鹽值,計算成本低 | 極低 |
PBKDF2 | 有鹽值,可調整迭代次數 | 可以GPU并行化(相對Bcrypt和Argon2而言) | 中高 |
Bcrypt | 有鹽值,內置成本因子,抗GPU攻擊 | 內存需求固定,輸出長度固定為192位 | 高 |
Argon2 | 可調整時間、內存、并行度,2015年密碼散列競賽冠軍 | 相對較新,庫支持不如PBKDF2廣泛 | 非常高 |
何時選擇PBKDF2:
- 當你需要一個廣泛支持、經過時間考驗的算法
- 當你需要符合某些特定標準(如FIPS)
- 當你需要生成特定長度的密鑰而不僅僅是密碼散列
第二部分:在Java和SpringBoot中使用PBKDF2
1. Java內置的PBKDF2支持
從Java 8開始,JDK提供了內置的PBKDF2實現,位于javax.crypto
包中:
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.util.Base64;public class PBKDF2Hasher {// 算法名稱private static final String ALGORITHM = "PBKDF2WithHmacSHA256";// 迭代次數private static final int ITERATIONS = 65536;// 密鑰長度private static final int KEY_LENGTH = 256;// 鹽值長度private static final int SALT_LENGTH = 16;// 生成鹽值public static byte[] generateSalt() {SecureRandom random = new SecureRandom();byte[] salt = new byte[SALT_LENGTH];random.nextBytes(salt);return salt;}// 使用PBKDF2生成散列值public static byte[] hash(char[] password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);return factory.generateSecret(spec).getEncoded();}// 驗證密碼public static boolean verify(char[] password, byte[] hash, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {byte[] testHash = hash(password, salt);return Arrays.equals(hash, testHash);}// 生成散列值和鹽值,并編碼為Base64字符串public static String[] hashPassword(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {byte[] salt = generateSalt();byte[] hash = hash(password.toCharArray(), salt);String hashString = Base64.getEncoder().encodeToString(hash);String saltString = Base64.getEncoder().encodeToString(salt);return new String[] { hashString, saltString };}// 從Base64字符串中驗證密碼public static boolean verifyPassword(String password, String hashString, String saltString) throws NoSuchAlgorithmException, InvalidKeySpecException {byte[] hash = Base64.getDecoder().decode(hashString);byte[] salt = Base64.getDecoder().decode(saltString);return verify(password.toCharArray(), hash, salt);}
}
2. SpringBoot中集成PBKDF2
在Spring Security中,從5.0版本開始,提供了內置的PBKDF2密碼編碼器Pbkdf2PasswordEncoder
。以下是在SpringBoot項目中集成PBKDF2的方法:
2.1 添加依賴
首先,確保你的SpringBoot項目中包含Spring Security依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 配置PBKDF2密碼編碼器
在Spring Security配置類中配置Pbkdf2PasswordEncoder
:
package com.example.pbkdf2demo.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;@Configuration
@EnableWebSecurity
public class SecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/public/**").permitAll().anyRequest().authenticated().and().formLogin().loginPage("/login").permitAll().and().logout().permitAll();return http.build();}@Beanpublic PasswordEncoder passwordEncoder() {// 創建PBKDF2密碼編碼器// 參數分別是:密鑰迭代次數、密鑰長度(比特)// Spring Security 5.x 版本return new Pbkdf2PasswordEncoder("", 185000, 256);// Spring Security 6.x 及更高版本// return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();}
}
2.3 自定義PBKDF2編碼器參數
如果需要更精細地控制PBKDF2參數,可以這樣配置:
@Bean
public PasswordEncoder passwordEncoder() {// SpringBoot 2.x 和 Spring Security 5.xString secret = "your-secret"; // 可選的密鑰int iterations = 210000; // 迭代次數int hashWidth = 512; // 哈希長度(比特)return new Pbkdf2PasswordEncoder(secret,iterations,hashWidth);// SpringBoot 3.x 和 Spring Security 6.x/*return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8().secret("your-secret").iterations(210000).hashWidth(512).algorithm(Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA512).build();*/
}
2.4 在用戶服務中使用PBKDF2編碼器
package com.example.pbkdf2demo.service;import com.example.pbkdf2demo.model.User;
import com.example.pbkdf2demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate PasswordEncoder passwordEncoder;public User registerUser(String username, String password) {// 判斷用戶名是否已存在if (userRepository.findByUsername(username).isPresent()) {throw new IllegalArgumentException("用戶名已存在");}// 使用PBKDF2編碼密碼String encodedPassword = passwordEncoder.encode(password);// 創建新用戶User user = new User();user.setUsername(username);user.setPassword(encodedPassword); // 存儲PBKDF2編碼后的密碼user.setEnabled(true);return userRepository.save(user);}public boolean authenticate(String username, String password) {return userRepository.findByUsername(username).map(user -> passwordEncoder.matches(password, user.getPassword())