1. 概述
在構建處理敏感數據(如用户密碼)的Web應用程序時,確保用户密碼的安全至關重要。 密碼安全的一個重要方面是檢查密碼是否被泄露,這通常是由於密碼出現在數據泄露事件中造成的。
Spring Security 6.3 引入了一個新功能,允許我們輕鬆檢查密碼是否被泄露。
在本教程中,我們將探索 Spring Security 中的 CompromisedPasswordChecker API 以及如何將其集成到我們的 Spring Boot 應用程序中。
2. 理解被盜密碼
被盜密碼是指在數據泄露中暴露的密碼,使其容易受到未經授權的訪問。攻擊者經常使用這些被盜密碼在憑據填充和密碼填充攻擊中,利用泄露的用户名-密碼組合在多個網站或常見密碼對多個帳户進行嘗試。
為了減輕這種風險,在創建帳户之前檢查用户密碼是否被盜至關重要。
請注意,一個以前有效的密碼可能會隨着時間的推移而變得被盜,因此建議在帳户創建過程以及允許用户更改密碼的登錄過程或任何過程中檢查被盜密碼。
3. 弱密碼檢查器 API
Spring Security 提供了一個簡單的 弱密碼檢查器 接口,用於檢查密碼是否被泄露:
public interface CompromisedPasswordChecker {
CompromisedPasswordDecision check(String password);
}
該接口暴露了一個名為 check() 的單方法,該方法接受密碼作為輸入並返回一個 弱密碼決定 實例,指示密碼是否被泄露。
check() 方法期望接收未加密的密碼,因此我們需要在加密我們的密碼之前調用它,使用 密碼編碼器。
3.1. 配置 弱密碼檢查器 Bean
為了在我們的應用程序中啓用弱密碼檢查,我們需要聲明一個類型為 弱密碼檢查器 的 Bean:
@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
return new HaveIBeenPwnedRestApiPasswordChecker();
}
HaveIBeenPwnedRestApiPasswordChecker 是 Spring Security 提供的默認 弱密碼檢查器 實現。
此默認實現與流行的 弱密碼檢查器 API 集成,該 API 維護了一個來自數據泄露的泄露密碼的廣泛數據庫。
當該默認實現中的 check() 方法被調用時,它會安全地對提供的密碼進行哈希,並將哈希的前 5 個字符發送到弱密碼檢查器 API。 API 會響應一個與此前綴匹配的後綴列表。 然後該方法將密碼的完整哈希與此列表進行比較,以確定它是否被泄露。 整個檢查是在不發送未加密的密碼到網絡上進行的。
3.2. 自定義 弱密碼檢查器 Bean
@Bean
public CompromisedPasswordChecker customCompromisedPasswordChecker() {
RestClient customRestClient = RestClient.builder()
.baseUrl("https://api.proxy.com/password-check")
.defaultHeader("X-API-KEY", "api-key")
.build();
HaveIBeenPwnedRestApiPasswordChecker compromisedPasswordChecker = new HaveIBeenPwnedRestApiPasswordChecker();
compromisedPasswordChecker.setRestClient(customRestClient);
return compromisedPasswordChecker;
}
現在,當我們調用我們的應用程序中 弱密碼檢查器 Bean 的 check() 方法時,它會將 API 請求發送到我們已定義的基 URL,並附帶自定義 HTTP header。
4. 處理已泄露密碼
現在我們已經配置了 CompromisedPasswordChecker Bean,讓我們看看如何將其在我們的服務層中使用來驗證密碼。 讓我們考慮一個常見的用例:新用户註冊:
@Autowired
private CompromisedPasswordChecker compromisedPasswordChecker;
String password = userCreationRequest.getPassword();
CompromisedPasswordDecision decision = compromisedPasswordChecker.check(password);
if (decision.isCompromised()) {
throw new CompromisedPasswordException("提供的密碼已泄露,無法使用。");
}
在這裏,我們只需調用 check() 方法,傳入客户端提供的明文密碼,並檢查返回的 CompromisedPasswordDecision。 如果 isCompromised() 方法返回 true,我們將拋出 CompromisedPasswordException 以中止註冊過程。
5. 處理 CompromisedPasswordException
當我們的服務層拋出 CompromisedPasswordException,我們希望優雅地處理它並向客户端提供反饋。
一種方法是定義一個全局異常處理程序在一個 @RestControllerAdvice 類中:
@ExceptionHandler(CompromisedPasswordException.class)
public ProblemDetail handle(CompromisedPasswordException exception) {
return ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exception.getMessage());
}
當這個處理方法捕獲一個 CompromisedPasswordException,它將返回 ProblemDetail 類的實例,該類構建了一個符合 RFC 9457 規範的錯誤響應:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "提供的密碼已泄露,無法使用。",
"instance": "/api/v1/users"
}
6. 自定義 CompromisedPasswordChecker 實現
雖然 HaveIBeenPwnedRestApiPasswordChecker 實現是一個不錯的解決方案,但在某些情況下,我們可能需要與不同的提供商集成,甚至實現自己的已泄露密碼檢查邏輯。
我們可以通過實現 CompromisedPasswordChecker 接口來完成:
public class PasswordCheckerSimulator implements CompromisedPasswordChecker {
public static final String FAILURE_KEYWORD = "compromised";
@Override
public CompromisedPasswordDecision check(String password) {
boolean isPasswordCompromised = false;
if (password.contains(FAILURE_KEYWORD)) {
isPasswordCompromised = true;
}
return new CompromisedPasswordDecision(isPasswordCompromised);
}
}
我們的示例實現認為,如果密碼包含單詞“compromised”,則密碼已泄露。雖然在實際場景中不太實用,但它展示瞭如何簡單地插入自己的自定義邏輯。
在我們的測試用例中,通常使用這樣的模擬實現而不是向外部 API 發送 HTTP 請求。要使用自定義實現在測試中,我們可以將其定義為 @TestConfiguration 類中的一個 Bean:
@TestConfiguration
public class TestSecurityConfiguration {
@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
return new PasswordCheckerSimulator();
}
}
在我們的測試類中,當我們想要使用此自定義實現時,我們會使用 @Import(TestSecurityConfiguration.class) 標註它。
此外,為了避免在運行測試時出現 BeanDefinitionOverrideException,我們會使用 @ConditionalOnMissingBean 註解標註我們的主 CompromisedPasswordChecker Bean。
最後,為了驗證自定義實現的行為,我們會編寫一個測試用例:
@Test
void whenPasswordCompromised_thenExceptionThrown() {
String emailId = RandomString.make() + "@baeldung.it";
String password = PasswordCheckerSimulator.FAILURE_KEYWORD + RandomString.make();
String requestBody = String.format("""
{
"emailId" : "%s",
"password" : "%s"
}
""", emailId, password);
String apiPath = "/users";
mockMvc.perform(post(apiPath).contentType(MediaType.APPLICATION_JSON).content(requestBody))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath("$.detail").value("The provided password is compromised and cannot be used."));
}
7. 創建自定義 @NotCompromised 註解
正如前面討論的,我們不僅應該在用户註冊期間檢查密碼是否受損,還應該在所有允許用户更改密碼或使用密碼進行身份驗證的 API 中進行檢查,例如登錄 API。
雖然我們可以為這些過程中的每個步驟執行此檢查,但 使用自定義驗證註解提供了一種更聲明式和可重用的方法。
首先,讓我們定義自定義 @NotCompromised 註解:
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CompromisedPasswordValidator.class)
public @interface NotCompromised {
String message() default "The provided password is compromised and cannot be used.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
接下來,讓我們實現 ConstraintValidator 接口:
public class CompromisedPasswordValidator implements ConstraintValidator<NotCompromised, String> {
@Autowired
private CompromisedPasswordChecker compromisedPasswordChecker;
@Override
public boolean isValid(String password, ConstraintValidatorContext context) {
CompromisedPasswordDecision decision = compromisedPasswordChecker.check(password);
return !decision.isCompromised();
}
}
我們注入一個 CompromisedPasswordChecker 類的實例,並使用它來檢查客户端的密碼是否受損。
現在,我們可以將自定義 @NotCompromised 註解應用於請求體中密碼字段,並驗證其值:
@NotCompromised
private String password;
@Autowired
private Validator validator;
UserCreationRequestDto request = new UserCreationRequestDto();
request.setEmailId(RandomString.make() + "@baeldung.it");
request.setPassword(PasswordCheckerSimulator.FAILURE_KEYWORD + RandomString.make());
Set<ConstraintViolation<UserCreationRequestDto>> violations = validator.validate(request);
assertThat(violations).isNotEmpty();
assertThat(violations)
.extracting(ConstraintViolation::getMessage)
.contains("The provided password is compromised and cannot be used.");
8. 結論
在本文中,我們探討了如何使用 Spring Security 的 CompromisedPasswordChecker API 來增強應用程序的安全性,通過檢測和防止使用已泄露密碼。
我們討論瞭如何配置默認的 HaveIBeenPwnedRestApiPasswordChecker 實現。我們還討論瞭如何針對我們的特定環境進行自定義,甚至實現自定義的已泄露密碼檢查邏輯。
總而言之,檢查已泄露密碼為用户賬户提供了一層額外的安全保護,抵禦潛在的安全攻擊。