使用 Spring Security 檢測外洩的密碼
1. 概述
在建立處理敏感資料的 Web 應用程式時,確保使用者密碼的安全非常重要。密碼安全的一個重要方面是檢查密碼是否被洩露,這通常是由於其存在於資料外洩中。
Spring Security 6.3引入了一項新功能,可以讓我們輕鬆檢查密碼是否已洩露。
在本教程中,我們將探索 Spring Security 中新的CompromisedPasswordChecker API 以及如何將其整合到我們的 Spring Boot 應用程式中。
2. 了解密碼洩露
洩漏的密碼是在資料外洩中暴露的密碼,使其容易受到未經授權的存取。攻擊者經常在憑證填充和密碼填充攻擊中使用這些洩漏的密碼,使用跨多個網站洩露的使用者名稱密碼對或針對多個帳戶的通用密碼。
為了降低這種風險,在建立帳戶之前檢查用戶的密碼是否被洩露至關重要。
還需要注意的是,隨著時間的推移,以前有效的密碼可能會洩露,因此始終建議不僅在帳戶創建期間,而且在登入過程或允許用戶更改密碼的任何過程中檢查密碼是否被洩露。如果因為偵測到密碼外洩而導致登入嘗試失敗,我們可以提示使用者重設密碼。
3.CompromishedPasswordChecker CompromisedPasswordChecker API
Spring Security 提供了一個簡單的CompromisedPasswordChecker介面來檢查密碼是否已被洩露:
public interface CompromisedPasswordChecker {
CompromisedPasswordDecision check(String password);
}
此介面公開了一個check()方法,該方法將密碼作為輸入並傳回CompromisedPasswordDecision的實例,指示密碼是否被洩露。
check()方法需要一個明文密碼,因此我們在使用PasswordEncoder加密密碼之前呼叫它。
3.1.配置CompromisedPasswordChecker Bean
為了在我們的應用程式中啟用洩漏密碼檢查,我們需要聲明CompromisedPasswordChecker類型的 bean:
@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
return new HaveIBeenPwnedRestApiPasswordChecker();
}
HaveIBeenPwnedRestApiPasswordChecker是 Spring Security 提供的CompromisedPasswordChecker的預設實作。
此預設實現與流行的Have I Been Pwned API集成,該 API 維護著一個包含因資料外洩而洩露的密碼的廣泛資料庫。
當呼叫此預設實作的check()方法時,它會安全地對所提供的密碼進行雜湊處理,並將雜湊值的前 5 個字元傳送到 Have I Been Pwned API。 API 使用與此前綴相符的雜湊後綴清單進行回應。然後,該方法將密碼的完整雜湊值與此列表進行比較,並確定密碼是否被洩露。整個檢查是在不透過網路發送明文密碼的情況下執行的。
3.2.自訂CompromisedPasswordChecker Bean
如果我們的應用程式使用代理伺服器進行出口 HTTP 請求,我們可以配置 使用自訂的HaveIBeenPwnedRestApiPasswordChecker RestClient :
@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;
}
現在,當我們在應用程式中呼叫CompromisedPasswordChecker bean 的check()方法時,它會將 API 請求傳送到我們定義的基本 URL 以及自訂 HTTP 標頭。
4. 處理洩漏的密碼
現在我們已經配置了CompromisedPasswordChecker bean,讓我們看看如何在服務層中使用它來驗證密碼。讓我們來看一個新用戶註冊的常見用例:
@Autowired
private CompromisedPasswordChecker compromisedPasswordChecker;
String password = userCreationRequest.getPassword();
CompromisedPasswordDecision decision = compromisedPasswordChecker.check(password);
if (decision.isCompromised()) {
throw new CompromisedPasswordException("The provided password is compromised and cannot be used.");
}
在這裡,我們只需使用客戶端提供的明文密碼呼叫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": "The provided password is compromised and cannot be used.",
"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);
}
}
如果密碼包含「compromied」一詞,我們的範例實作就會認為該密碼已外洩。雖然在現實場景中不是很有用,但它演示了插入我們自己的自訂邏輯是多麼簡單。
在我們的測試案例中,使用此類模擬實作而不是對外部 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.");
八、結論
在本文中,我們探討如何使用 Spring Security 的CompromisedPasswordChecker API 透過偵測和防止使用受損密碼來增強應用程式的安全性。
我們討論如何配置預設的HaveIBeenPwnedRestApiPasswordChecker實作。我們還討論了針對我們的特定環境對其進行自定義,甚至實現我們自己的自訂受損密碼檢查邏輯。
總而言之,檢查洩漏的密碼為我們的使用者帳戶增加了一層額外的保護,防止潛在的安全攻擊。
與往常一樣,本文中使用的所有程式碼範例都可以在 GitHub 上找到。