1. 簡介
登錄表單長期以來一直是任何需要身份驗證以提供其服務的 Web 服務中常見的特性。然而,隨着安全問題日益突出,變得清晰,簡單的文本密碼是一個弱點:它們可以被猜測、攔截或泄露,從而導致安全事件,可能導致財務和/或聲譽損害。
以往嘗試用替代方案(如 mTLS、安全卡等)替換密碼,雖然試圖解決這個問題,但卻導致了糟糕的用户體驗和額外的成本。
在本教程中,我們將探索 Passkeys,也稱為 WebAuthn,這是一個提供密碼安全替代方案的標準。 尤其是,我們將演示如何快速地將此身份驗證機制添加到 Spring Boot 應用程序中,使用 Spring Security。
2. 什麼是 Passkey?
Passkey 或 WebAuthn 是一種由 W3C 聯盟定義的標準 API,允許在 Web 瀏覽器上運行的應用程序管理公共密鑰並將其註冊用於與特定服務提供商一起使用。
典型的註冊場景如下:
- 用户在服務上創建一個新帳户。初始憑據通常是熟悉的用户名/密碼
- 註冊後,用户轉到個人資料頁面並選擇“創建 Passkey”
- 系統顯示 Passkey 註冊表單
- 用户填寫表單,輸入所需的信息——例如,將幫助用户稍後選擇正確密鑰的密鑰標籤,然後提交表單
- 系統將 Passkey 保存到數據庫中,並將其與用户帳户關聯。同時,該密鑰的私有部分也將保存在用户的設備上
- Passkey 註冊完成
一旦密鑰註冊完成,用户就可以使用存儲的 Passkey 訪問該服務。根據瀏覽器的安全配置和用户的設備配置,登錄可能需要指紋掃描、解鎖智能手機或類似操作。
Passkey 包含兩部分:瀏覽器發送給服務提供商的公共密鑰以及保存在本地設備上的私有部分。
此外,客户端 API 的實現確保給定 Passkey 只能與註冊它的同一站點使用。
3. 在 Spring Boot 應用中添加 Passkeys
讓我們創建一個簡單的 Spring Boot 應用來測試 Passkeys。 我們的應用將僅包含一個歡迎頁面,顯示當前用户的姓名以及指向 Passkeys 註冊頁面的鏈接。
第一步是向項目添加所需的依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>com.webauthn4j</groupId>
<artifactId>webauthn4j-core</artifactId>
<version>0.28.5.RELEASE</version>
</dependency>最新版本的這些依賴項可在 Maven Central 上獲取:
重要提示: WebAuthn 支持需要 Spring Boot 3.4.0 或更高版本。
4. Spring Security 配置
從 Spring Security 6.4 開始(這是通過 spring-boot-starter-security 依賴項默認提供的版本),配置 DSL 具有原生對 passkeys 的支持,通過 webautn() 方法實現。
@Bean
SecurityFilterChain webauthnFilterChain(HttpSecurity http, WebAuthNProperties webAuthNProperties) {
return http.authorizeHttpRequests( ht -> ht.anyRequest().authenticated())
.formLogin(withDefaults())
.webAuthn(webauth ->
webauth.allowedOrigins(webAuthNProperties.getAllowedOrigins())
.rpId(webAuthNProperties.getRpId())
.rpName(webAuthNProperties.getRpName())
)
.build();
}
以下是該配置所帶來的結果:
- 登錄頁面上將存在一個“使用 passkey 登錄”按鈕
- 註冊頁面位於 /webauthn/register
為了確保正常運行,必須向 webauthn 配置器提供至少以下配置屬性:
- allowedOrigins:該站點(必須使用 HTTPS)的外部 URL,除非它使用 localhost
- rpId:應用程序標識符,必須是與 hostname 部分的 allowedOrigin 屬性相匹配的有效域名
- rpName:瀏覽器在註冊和/或登錄過程中可能使用的用户友好的名稱
然而,此配置缺少 passkey 支持的一個關鍵方面:應用程序重啓後已註冊的密鑰將丟失。 這是因為 Spring Security 默認使用基於內存的身份驗證存儲,該存儲不適用於生產環境。
稍後我們將看到如何解決這個問題。
5. Passkey Walk-Around
配置 Passkey 之後,現在讓我們快速瀏覽一下我們的應用程序。使用 mvn spring-boot:run 或 IDE 啓動它後,我們可以在瀏覽器中打開它,並導航到 http://localhost:8080。
Spring 應用程序的標準登錄頁面現在將包含“使用 Passkey 登錄”按鈕。 由於我們尚未註冊任何密鑰,因此必須使用用户名/密碼憑據登錄,這些憑據已配置在我們的 application.yaml 文件中:alice/changeit
正如預期的那樣,我們現在已登錄為 Alice。現在我們可以繼續到註冊頁面,單擊“註冊 PassKey”鏈接:
在這裏,我們只需提供一個標籤——baeldung-demo——然後單擊“註冊”。接下來發生的事情取決於設備類型(桌面、移動設備、平板電腦)和操作系統(Windows、Linux、Mac、Android),但最終結果將是新的密鑰添加到列表中:
例如,在 Windows 上的 Chrome 中,對話框將提供創建新密鑰和將其存儲在瀏覽器原生密碼管理器中或使用 Windows Hello 功能中的選項。
接下來,我們退出應用程序並嘗試我們的新密鑰。首先,我們導航到 http://localhost:8080/logout 並確認要退出。然後,在登錄表單上,我們單擊“使用 Passkey 登錄”。瀏覽器將顯示一個對話框,允許您選擇一個 Passkey:
一旦我們選擇了可用的密鑰之一,設備將執行額外的身份驗證挑戰。對於“Windows Hello”身份驗證,這可能包括指紋掃描、人臉識別等。
如果身份驗證成功,用户的私鑰將用於對挑戰進行簽名並將其發送到服務器,然後服務器將使用先前存儲的公鑰進行驗證。最後,如果所有內容都通過驗證,登錄將完成,歡迎頁面將顯示如前所述。
6. Passkey 存儲庫
正如之前所述,Spring Security 默認創建的 passkey 配置方案不提供已註冊密鑰的持久化存儲。 為了解決這個問題,我們需要提供對以下接口的實現:
- PublicKeyCredentialUserEntityRepository
- UserCredentialRepository
6.1. PublicKeyCredentialUserEntityRepository
本服務管理 PublicKeyCredentialUserEntity 實例,並將標準 UserDetailsService 管理的用户賬户映射為用户賬户標識符。該實體具有以下屬性:
- name:用於賬户的友好的標識名稱
- id:用户賬户的異名標識
- displayName:用於顯示目的的賬户名稱的替代版本
請注意,當前實現假設在給定的身份驗證域內,name 和 id 都是唯一的。
通常,此表中條目與由標準 UserDetailsService 管理的賬户之間存在 1:1 的關係。
該實現,可在網上找到,使用 Spring Data JDBC 存儲這些字段到 PASSKEY_USERS 表中。
6.2. UserCredentialRepository
管理 CredentialRecord 實例,該實例存儲了在註冊過程中從瀏覽器接收的實際公鑰。該實體包含所有推薦的屬性,如 W3C 文檔中指定,以及一些額外的屬性:
- userEntityUserId: 屬於該憑證的 PublicKeyCredentialUserEntity 標識符
- label: 在註冊時分配給該憑證的,由用户定義的標籤
- lastUsed: 該憑證的最後使用日期
- created: 該憑證的創建日期
請注意,CredentialRecord 與 PublicKeyCredentialUserEntity 之間存在 N:1 關係,這體現在該倉庫的方法上。例如,findByUserId() 方法返回一個 CredentialRecord 實例的列表。
我們的實現考慮到了這一點,並使用 PASSKEY_CREDENTIALS 表中的外鍵,以確保參照完整性。
7. 測試
雖然可以使用模擬請求測試基於 passkey 的應用程序,但這些測試的價值有限。大多數故障場景與客户端相關,因此需要使用驅動真實瀏覽器的集成測試,藉助自動化工具進行驅動。
在這裏,我們將使用 Selenium 實現一個“正常流程”場景,僅用於説明該技術。特別是,我們將使用 VirtualAuthenticator 功能來配置 WebDriver,從而模擬註冊和登錄頁面之間的交互,利用這種機制。
例如,以下是如何使用 VirtualAuthenticator 創建新的驅動程序:
@BeforeEach
void setupTest() {
VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions()
.setIsUserVerified(true)
.setIsUserConsenting(true)
.setProtocol(VirtualAuthenticatorOptions.Protocol.CTAP2)
.setHasUserVerification(true)
.setHasResidentKey(true);
driver = new ChromeDriver();
authenticator = ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options);
}
一旦我們獲取了 驗證器實例,我們就可以使用它來模擬不同的場景,例如成功的登錄、註冊等。我們的現場測試會經歷一個完整的流程,包括以下步驟:
- 使用用户名/密碼憑據進行初始登錄
- Passkey 註冊
- 註銷
- 使用 Passkey 登錄
8. 結論
在本教程中,我們演示瞭如何在 Spring Boot Web 應用程序中使用 Passkeys 的方法,包括 Spring Security 的配置和為實際應用所需的密鑰持久化支持。