密碼儲存
Spring Security 的 PasswordEncoder 介面用於對密碼執行單向轉換,以使密碼能夠安全儲存。由於 PasswordEncoder 是單向轉換,因此當密碼轉換需要雙向時(例如儲存用於驗證資料庫的憑據)它沒有用。通常,PasswordEncoder 用於儲存需要與使用者在身份驗證時提供的密碼進行比較的密碼。
密碼儲存歷史
多年來,密碼儲存的標準機制不斷發展。最初,密碼以純文字形式儲存。密碼被認為是安全的,因為儲存密碼的資料儲存需要憑據才能訪問。然而,惡意使用者能夠透過 SQL 注入等攻擊找到獲取大量“資料轉儲”使用者名稱和密碼的方法。隨著越來越多的使用者憑據被公開,安全專家意識到我們需要做更多的工作來保護使用者的密碼。
然後鼓勵開發人員在透過 SHA-256 等單向雜湊處理後儲存密碼。當用戶嘗試進行身份驗證時,雜湊密碼將與他們輸入的密碼的雜湊進行比較。這意味著系統只需要儲存密碼的單向雜湊。如果發生洩露,只會暴露密碼的單向雜湊。由於雜湊是單向的,並且在給定雜湊的情況下猜測密碼計算起來很困難,因此不值得付出努力來找出系統中的每個密碼。為了擊敗這個新系統,惡意使用者決定建立稱為 彩虹表 的查詢表。他們不是每次都進行猜測每個密碼的工作,而是一次計算密碼並將其儲存在查詢表中。
為了減輕彩虹表的有效性,鼓勵開發人員使用加鹽密碼。不是僅僅使用密碼作為雜湊函式的輸入,而是為每個使用者的密碼生成隨機位元組(稱為鹽)。鹽和使用者密碼將透過雜湊函式執行以生成唯一的雜湊。鹽將與使用者密碼以明文形式儲存。然後,當用戶嘗試進行身份驗證時,雜湊密碼將與儲存的鹽和他們輸入的密碼的雜湊進行比較。獨特的鹽意味著彩虹表不再有效,因為每個鹽和密碼組合的雜湊都不同。
在現代,我們意識到密碼雜湊(如 SHA-256)不再安全。原因是使用現代硬體,我們可以每秒執行數十億次雜湊計算。這意味著我們可以輕鬆地單獨破解每個密碼。
現在鼓勵開發人員利用自適應單向函式來儲存密碼。使用自適應單向函式驗證密碼是故意資源密集型的(它們故意使用大量 CPU、記憶體或其他資源)。自適應單向函式允許配置一個“工作因子”,該因子可以隨著硬體的改進而增長。我們建議將“工作因子”調整為大約一秒鐘在您的系統上驗證一個密碼。這種權衡是為了使攻擊者難以破解密碼,但又不會代價過高,從而給您的系統帶來過大的負擔或惹惱使用者。Spring Security 試圖為“工作因子”提供一個好的起點,但我們鼓勵使用者為自己的系統自定義“工作因子”,因為效能因系統而異。應使用的自適應單向函式的示例包括 bcrypt、PBKDF2、scrypt 和 argon2。
由於自適應單向函式故意是資源密集型的,因此為每個請求驗證使用者名稱和密碼會顯著降低應用程式的效能。Spring Security(或任何其他庫)無法加快密碼驗證的速度,因為安全性是透過使驗證資源密集型來獲得的。鼓勵使用者將長期憑據(即使用者名稱和密碼)換成短期憑據(例如會話、OAuth 令牌等)。短期憑據可以快速驗證,而不會損失任何安全性。
DelegatingPasswordEncoder
在 Spring Security 5.0 之前,預設的 PasswordEncoder 是 NoOpPasswordEncoder,它需要純文字密碼。根據 密碼歷史 部分,您可能期望預設的 PasswordEncoder 現在是 BCryptPasswordEncoder 之類的東西。然而,這忽略了三個現實世界的問題:
-
許多應用程式使用舊的密碼編碼,這些編碼無法輕易遷移。
-
密碼儲存的最佳實踐將再次改變。
-
作為框架,Spring Security 無法頻繁進行重大更改。
相反,Spring Security 引入了 DelegatingPasswordEncoder,它透過以下方式解決了所有這些問題:
-
確保使用當前密碼儲存建議對密碼進行編碼
-
允許驗證現代和傳統格式的密碼
-
允許將來升級編碼
您可以使用 PasswordEncoderFactories 輕鬆構建 DelegatingPasswordEncoder 例項
-
Java
-
Kotlin
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
或者,您可以建立自己的自定義例項
-
Java
-
Kotlin
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()
encoders["pbkdf2@SpringSecurity_v5_8"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["scrypt"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()
encoders["scrypt@SpringSecurity_v5_8"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["argon2"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()
encoders["argon2@SpringSecurity_v5_8"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["sha256"] = StandardPasswordEncoder()
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
密碼儲存格式
密碼的一般格式是
{id}encodedPassword
id 是一個識別符號,用於查詢應該使用哪個 PasswordEncoder,而 encodedPassword 是所選 PasswordEncoder 的原始編碼密碼。id 必須位於密碼的開頭,以 { 開頭,並以 } 結尾。如果找不到 id,則將 id 設定為 null。例如,以下可能是一個使用不同 id 值編碼的密碼列表。所有原始密碼都是 password。
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)
| 1 | 第一個密碼的 PasswordEncoder ID 為 bcrypt,encodedPassword 值為 $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。匹配時,它將委託給 BCryptPasswordEncoder |
| 2 | 第二個密碼的 PasswordEncoder ID 為 noop,encodedPassword 值為 password。匹配時,它將委託給 NoOpPasswordEncoder |
| 3 | 第三個密碼的 PasswordEncoder ID 為 pbkdf2,encodedPassword 值為 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。匹配時,它將委託給 Pbkdf2PasswordEncoder |
| 4 | 第四個密碼的 PasswordEncoder ID 為 scrypt,encodedPassword 值為 $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=。匹配時,它將委託給 SCryptPasswordEncoder |
| 5 | 最後一個密碼的 PasswordEncoder ID 為 sha256,encodedPassword 值為 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。匹配時,它將委託給 StandardPasswordEncoder |
|
一些使用者可能擔心儲存格式會提供給潛在的駭客。這不是一個問題,因為密碼的儲存不依賴於演算法是秘密。此外,大多數格式攻擊者無需字首即可輕鬆識別。例如,BCrypt 密碼通常以 |
密碼編碼
傳入建構函式的 idForEncode 決定了用於編碼密碼的 PasswordEncoder。在我們之前構建的 DelegatingPasswordEncoder 中,這意味著編碼 password 的結果將委託給 BCryptPasswordEncoder,並以 {bcrypt} 為字首。最終結果如下例所示:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
密碼匹配
匹配基於 {id} 以及 id 到建構函式中提供的 PasswordEncoder 的對映。我們在 密碼儲存格式 中的示例提供了一個說明其工作原理的示例。預設情況下,使用密碼和未對映的 id(包括空 id)呼叫 matches(CharSequence, String) 的結果會導致 IllegalArgumentException。此行為可以透過使用 DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) 進行自定義。
透過使用 id,我們可以匹配任何密碼編碼,但使用最現代的密碼編碼對密碼進行編碼。這很重要,因為與加密不同,密碼雜湊的設計使得沒有簡單的方法可以恢復純文字。由於無法恢復純文字,因此很難遷移密碼。雖然使用者遷移 NoOpPasswordEncoder 很簡單,但我們選擇預設包含它以使其入門體驗更簡單。
入門體驗
如果您正在製作演示或示例,花時間雜湊使用者的密碼會有點麻煩。有一些便利機制可以使其更容易,但這仍然不適用於生產環境。
-
Java
-
Kotlin
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
如果您正在建立多個使用者,您還可以重用構建器
-
Java
-
Kotlin
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER","ADMIN")
.build();
val users = User.withDefaultPasswordEncoder()
val user = users
.username("user")
.password("password")
.roles("USER")
.build()
val admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build()
這確實會對儲存的密碼進行雜湊處理,但密碼仍會暴露在記憶體和編譯後的原始碼中。因此,它仍然不被認為是生產環境的安全措施。對於生產環境,您應該 外部雜湊您的密碼。
使用 Spring Boot CLI 編碼
正確編碼密碼最簡單的方法是使用 Spring Boot CLI。
例如,以下示例對 password 密碼進行編碼,以便與 DelegatingPasswordEncoder 一起使用
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
故障排除
當儲存的密碼之一沒有 id 時,會出現以下錯誤,如 密碼儲存格式 中所述。
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
解決它的最簡單方法是弄清楚您的密碼目前是如何儲存的,並明確提供正確的 PasswordEncoder。
如果您正在從 Spring Security 4.2.x 遷移,您可以透過 公開 NoOpPasswordEncoder bean 來恢復到以前的行為。
或者,您可以為所有密碼加上正確的 id 字首,並繼續使用 DelegatingPasswordEncoder。例如,如果您正在使用 BCrypt,您將把密碼從類似以下內容遷移:
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
到
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
有關對映的完整列表,請參閱 PasswordEncoderFactories 的 Javadoc。
BCryptPasswordEncoder
BCryptPasswordEncoder 實現使用廣泛支援的 bcrypt 演算法對密碼進行雜湊處理。為了使其更能抵抗密碼破解,bcrypt 被故意設計為緩慢的。像其他自適應單向函式一樣,它應該調整為在您的系統上驗證密碼大約需要 1 秒。BCryptPasswordEncoder 的預設實現使用強度 10,如 BCryptPasswordEncoder 的 Javadoc 中所述。建議您在自己的系統上調整和測試強度引數,以便驗證密碼大約需要 1 秒。
-
Java
-
Kotlin
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with strength 16
val encoder = BCryptPasswordEncoder(16)
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Argon2PasswordEncoder
Argon2PasswordEncoder 實現使用 Argon2 演算法對密碼進行雜湊處理。Argon2 是 密碼雜湊競賽 的獲勝者。為了抵禦定製硬體上的密碼破解,Argon2 是一種故意緩慢的演算法,需要大量的記憶體。像其他自適應單向函式一樣,它應該調整為在您的系統上驗證密碼大約需要 1 秒。Argon2PasswordEncoder 的當前實現需要 BouncyCastle。
-
Java
-
Kotlin
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder 實現使用 PBKDF2 演算法對密碼進行雜湊處理。為了抵禦密碼破解,PBKDF2 是一種故意緩慢的演算法。像其他自適應單向函式一樣,它應該調整為在您的系統上驗證密碼大約需要 1 秒。當需要 FIPS 認證時,此演算法是一個不錯的選擇。
-
Java
-
Kotlin
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
SCryptPasswordEncoder
SCryptPasswordEncoder 實現使用 scrypt 演算法對密碼進行雜湊處理。為了抵禦定製硬體上的密碼破解,scrypt 是一種故意緩慢的演算法,需要大量的記憶體。像其他自適應單向函式一樣,它應該調整為在您的系統上驗證密碼大約需要 1 秒。
-
Java
-
Kotlin
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
其他 PasswordEncoder
存在大量其他 PasswordEncoder 實現,它們完全是為了向後相容。它們都已被棄用,以表明它們不再被認為是安全的。但是,目前沒有計劃刪除它們,因為遷移現有的遺留系統很困難。
基於 Password4j 的密碼編碼器
Spring Security 7.0 引入了基於 Password4j 庫的替代密碼編碼器實現。這些編碼器為流行的雜湊演算法提供了額外的選項,可以作為現有 Spring Security 實現的替代品。
Password4j 庫是一個 Java 加密庫,專注於密碼雜湊並支援多種演算法。當您需要特定的演算法配置或希望利用 Password4j 的最佳化時,這些編碼器特別有用。
所有基於 Password4j 的編碼器都是執行緒安全的,可以在多個執行緒之間共享。
Argon2Password4jPasswordEncoder
Argon2Password4jPasswordEncoder 實現透過 Password4j 庫使用 Argon2 演算法對密碼進行雜湊處理。這提供了 Spring Security 內建 Argon2PasswordEncoder 的替代方案,具有不同的配置選項和潛在的效能特徵。
Argon2 是 密碼雜湊競賽 的獲勝者,建議用於新應用程式。此實現利用 Password4j 的 Argon2 支援,該支援在輸出雜湊中正確包含鹽。
使用預設設定建立編碼器
-
Java
-
Kotlin
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder()
val result = encoder.encode("myPassword")
assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定義 Argon2 引數建立編碼器
-
Java
-
Kotlin
Argon2Function argon2Fn = Argon2Function.getInstance(65536, 3, 4, 32,
Argon2.ID);
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder(argon2Fn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val argon2Fn = Argon2Function.getInstance(
65536, 3, 4, 32,
Argon2.ID
)
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder(argon2Fn)
val result = encoder.encode("myPassword")
assertThat(encoder.matches("myPassword", result)).isTrue()
BcryptPassword4jPasswordEncoder
BcryptPassword4jPasswordEncoder 實現透過 Password4j 庫使用 BCrypt 演算法對密碼進行雜湊處理。這提供了 Spring Security 內建 BCryptPasswordEncoder 的替代方案,具有 Password4j 的實現特性。
BCrypt 是一種成熟的密碼雜湊演算法,它包含內建的鹽生成功能,並且能夠抵抗彩虹表攻擊。此實現利用 Password4j 的 BCrypt 支援,該支援在輸出雜湊中正確包含鹽。
使用預設設定建立編碼器
-
Java
-
Kotlin
PasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = BCryptPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定義 bcrypt 引數建立編碼器
-
Java
-
Kotlin
BcryptFunction bcryptFn = BcryptFunction.getInstance(12);
PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(bcryptFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val bcryptFunction = BcryptFunction.getInstance(12)
val encoder: PasswordEncoder = BcryptPassword4jPasswordEncoder(bcryptFunction)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
ScryptPassword4jPasswordEncoder
ScryptPassword4jPasswordEncoder 實現透過 Password4j 庫使用 SCrypt 演算法對密碼進行雜湊處理。這提供了 Spring Security 內建 SCryptPasswordEncoder 的替代方案,具有 Password4j 的實現特性。
SCrypt 是一種記憶體密集型密碼雜湊演算法,旨在抵抗硬體暴力破解攻擊。此實現利用 Password4j 的 SCrypt 支援,該支援在輸出雜湊中正確包含鹽。
使用預設設定建立編碼器
-
Java
-
Kotlin
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定義 scrypt 引數建立編碼器
-
Java
-
Kotlin
ScryptFunction scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32);
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(scryptFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32)
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder(scryptFn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
Pbkdf2Password4jPasswordEncoder
Pbkdf2Password4jPasswordEncoder 實現透過 Password4j 庫使用 PBKDF2 演算法對密碼進行雜湊處理。這提供了 Spring Security 內建 Pbkdf2PasswordEncoder 的替代方案,具有顯式鹽管理功能。
PBKDF2 是一種金鑰派生函式,旨在透過計算密集型來阻止字典和暴力攻擊。此實現顯式處理鹽管理,因為 Password4j 的 PBKDF2 實現不包括輸出雜湊中的鹽。編碼密碼格式為:{salt}:{hash},其中 salt 和 hash 均採用 Base64 編碼。
使用預設設定建立編碼器
-
Java
-
Kotlin
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定義 PBKDF2 引數建立編碼器
-
Java
-
Kotlin
PBKDF2Function pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256);
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(pbkdf2Fn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256)
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder(pbkdf2Fn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
BalloonHashingPassword4jPasswordEncoder
BalloonHashingPassword4jPasswordEncoder 實現透過 Password4j 庫使用 Balloon 雜湊演算法對密碼進行雜湊處理。Balloon 雜湊是一種記憶體密集型密碼雜湊演算法,旨在抵抗時間-記憶體權衡攻擊和側通道攻擊。
此實現顯式處理鹽管理,因為 Password4j 的 Balloon 雜湊實現不包括輸出雜湊中的鹽。編碼密碼格式為:{salt}:{hash},其中 salt 和 hash 均採用 Base64 編碼。
使用預設設定建立編碼器
-
Java
-
Kotlin
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定義引數建立編碼器
-
Java
-
Kotlin
BalloonHashingFunction ballooningHashingFn =
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3);
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val ballooningHashingFn =
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3)
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
密碼儲存配置
Spring Security 預設使用 DelegatingPasswordEncoder。但是,您可以透過將 PasswordEncoder 公開為 Spring bean 來對其進行自定義。
如果您正在從 Spring Security 4.2.x 遷移,您可以透過公開 NoOpPasswordEncoder bean 來恢復到以前的行為。
|
恢復到 |
-
Java
-
XML
-
Kotlin
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
<b:bean id="passwordEncoder"
class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance();
}
|
XML 配置要求 |
更改密碼配置
大多數允許使用者指定密碼的應用程式也需要更新密碼的功能。
一個用於更改密碼的已知 URL 指示了一種機制,密碼管理器可以透過該機制發現給定應用程式的密碼更新端點。
您可以配置 Spring Security 以提供此發現端點。例如,如果您的應用程式中的更改密碼端點是 /change-password,那麼您可以像這樣配置 Spring Security:
-
Java
-
XML
-
Kotlin
http
.passwordManagement(Customizer.withDefaults())
<sec:password-management/>
http {
passwordManagement { }
}
然後,當密碼管理器導航到 /.well-known/change-password 時,Spring Security 將重定向到您的端點 /change-password。
或者,如果您的端點不是 /change-password,您也可以像這樣指定:
-
Java
-
XML
-
Kotlin
http
.passwordManagement((management) -> management
.changePasswordPage("/update-password")
)
<sec:password-management change-password-page="/update-password"/>
http {
passwordManagement {
changePasswordPage = "/update-password"
}
}
透過上述配置,當密碼管理器導航到 /.well-known/change-password 時,Spring Security 將重定向到 /update-password。
密碼洩露檢查
在某些情況下,您需要檢查密碼是否已洩露,例如,如果您正在建立一個處理敏感資料的應用程式,通常需要對使用者密碼執行一些檢查以確定其可靠性。其中一個檢查是密碼是否已洩露,通常是因為它已在 資料洩露 中被發現。
為了方便這一點,Spring Security 透過 HaveIBeenPwnedRestApiPasswordChecker 實現了 Have I Been Pwned API 的整合,該實現是 CompromisedPasswordChecker 介面的。
您可以自己使用 CompromisedPasswordChecker API,或者,如果您透過 Spring Security 身份驗證機制 使用 DaoAuthenticationProvider,則可以提供一個 CompromisedPasswordChecker bean,它將由 Spring Security 配置自動選取。
透過這樣做,當您嘗試透過表單登入使用弱密碼(例如 123456)進行身份驗證時,您將收到 401 錯誤或被重定向到 /login?error 頁面(具體取決於您的使用者代理)。然而,在這種情況下,僅僅 401 或重定向並不是很有用,它會導致一些混亂,因為使用者提供了正確的密碼但仍然不允許登入。在這種情況下,您可以透過 AuthenticationFailureHandler 處理 CompromisedPasswordException 以執行您想要的邏輯,例如將使用者代理重定向到 /reset-password:
-
Java
-
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin((login) -> login
.failureHandler(new CompromisedPasswordAuthenticationFailureHandler())
);
return http.build();
}
@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
return new HaveIBeenPwnedRestApiPasswordChecker();
}
static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler(
"/login?error");
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof CompromisedPasswordException) {
this.redirectStrategy.sendRedirect(request, response, "/reset-password");
return;
}
this.defaultFailureHandler.onAuthenticationFailure(request, response, exception);
}
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
formLogin {
authenticationFailureHandler = CompromisedPasswordAuthenticationFailureHandler()
}
}
return http.build()
}
@Bean
open fun compromisedPasswordChecker(): CompromisedPasswordChecker {
return HaveIBeenPwnedRestApiPasswordChecker()
}
class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler {
private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error")
private val redirectStrategy = DefaultRedirectStrategy()
override fun onAuthenticationFailure(
request: HttpServletRequest,
response: HttpServletResponse,
exception: AuthenticationException
) {
if (exception is CompromisedPasswordException) {
redirectStrategy.sendRedirect(request, response, "/reset-password")
return
}
defaultFailureHandler.onAuthenticationFailure(request, response, exception)
}
}