將金鑰整合到 Spring Security
1. 簡介
登入表單長期以來一直是、並且現在仍然是任何需要身份驗證才能提供服務的 Web 服務的常見功能。然而,隨著安全問題開始成為主流,簡單的文字密碼顯然是一個弱點:它們可能被猜測、攔截或洩露,導致安全事件,造成財務和/或聲譽損失。
之前嘗試用替代解決方案(mTLS、安全卡等)替換密碼來解決此問題,但導致用戶體驗不佳和額外成本。
在本教程中,我們將探索 Passkeys(也稱為 WebAuthn),這是提供密碼安全替代方案的標準。具體來說,我們將示範如何使用 Spring Security 快速為 Spring Boot 應用程式添加對此身份驗證機制的支援。
2. 什麼是密鑰?
Passkeys 或 WebAuthn 是由 W3C 聯盟定義的標準 API,允許在 Web 瀏覽器上執行的應用程式管理公鑰並將其註冊以供給定服務提供者使用。
典型的註冊場景如下:
- 用戶在該服務上建立一個新帳戶。初始憑證通常是熟悉的使用者名稱/密碼
- 註冊後,用戶進入個人資料頁面並選擇“建立密鑰”
- 系統顯示密鑰註冊表
- 使用者在表單中填寫所需資訊(例如,可協助使用者稍後選擇正確金鑰的金鑰標籤),然後提交
- 系統將密鑰保存在其資料庫中並將其與使用者帳戶關聯。同時,該密鑰的私有部分將保存在使用者的裝置上
- 密鑰註冊已完成
一旦密鑰註冊完成,用戶就可以使用儲存的密鑰存取服務。根據瀏覽器和用戶設備的安全配置,登入將需要指紋掃描、解鎖智慧型手機或類似操作。
密鑰由兩部分組成:瀏覽器發送給服務提供者的公鑰和保留在本地設備上的私鑰部分。
此外,客戶端 API 的實作確保給定的金鑰只能在註冊它的相同網站上使用。
3.向 Spring Boot 應用程式新增金鑰
讓我們建立一個簡單的 Spring Boot 應用程式來測試金鑰。我們的應用程式只有一個歡迎頁面,其中顯示目前使用者的姓名和指向金鑰註冊頁面的連結。
第一步是為專案新增所需的依賴項:
<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 上找到:
-
[spring-boot-starter-web](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web)
-
[spring-boot-starter-security](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security)
-
[webauthn4j-core](https://mvnrepository.com/artifact/com.webauthn4j/webauthn4j-core)
重要提示:WebAuthn 支援需要 Spring Boot 3.4.0 或更高版本
4.Spring Security 配置
從 Spring Security 6.4 開始(這是透過spring-boot-starter-security
依賴項包含的預設版本),設定 DSL 透過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();
}
這是我們透過這種配置得到的結果:
- 登入頁面上將顯示「使用金鑰登入」按鈕
- 註冊頁面位於
/webauthn/register
為了正常運行,我們必須至少向webauthn
配置器提供以下配置屬性:
-
allowedOrigins
:網站的外部 URL,必須使用 HTTPS,除非使用 localhost -
rpId
:應用程式標識符,必須是與allowedOrigin
屬性的hostname
部分相符的有效域名 -
rpName
:瀏覽器在註冊和/或登入過程中可能使用的使用者友善名稱
但是,這種配置缺少密鑰支援的一個關鍵方面:應用程式重新啟動後註冊的密鑰會遺失。這是因為,預設情況下,Spring Security 使用基於記憶體的實現憑證存儲,不適合生產使用。
稍後我們將看看如何修復這個問題。
5. 密鑰巡查
密鑰配置完成後,就可以快速瀏覽我們的應用程式了。一旦我們使用mvn spring-boot:run
或 IDE 啟動它,我們就可以打開瀏覽器並導航到[http://localhost:8080](http://localhost:8080) :
Spring 應用程式的標準登入頁面現在將包含「使用金鑰登入」按鈕。由於我們尚未註冊任何金鑰,因此我們必須使用使用者名稱/密碼憑證登錄,該憑證已在application.yaml
檔案中設定: alice/changeit
正如預期的那樣,我們現在以 Alice 登入。我們現在可以透過點擊Register PassKey
連結繼續進入註冊頁面:
在這裡,我們只需提供一個標籤 – baeldung-demo
– 然後點擊Register
。接下來發生的事情取決於設備類型(桌上型電腦、手機、平板電腦)和作業系統(Windows、Linux、Mac、Android),但最終會導致新密鑰添加到清單中:
例如,在 Windows 上的 Chrome 中,對話方塊將提供建立新金鑰並將其儲存到瀏覽器的本機密碼管理器中或使用作業系統上提供的Windows Hello功能的選擇。
接下來,讓我們退出應用程式並嘗試我們的新金鑰。首先,我們導覽至http://localhost:8080/logout
並確認要退出。接下來,在登入表單上,我們點擊「使用密碼登入」。瀏覽器將顯示一個對話框,讓您選擇密鑰:
一旦我們選擇其中一個可用金鑰,裝置將執行額外的身份驗證質詢。對於「Windows Hello」身份驗證,這可以是指紋掃描、臉部辨識等。
如果身份驗證成功,則使用者的私鑰將用於簽署質詢並將其發送到伺服器,伺服器將使用先前儲存的公鑰進行驗證。最後,如果一切檢查無誤,則登入完成並且歡迎頁面將像以前一樣顯示。
6. 密鑰儲存庫
如前所述,Spring Security 建立的預設金鑰配置不為已註冊的金鑰提供持久性。為了解決這個問題,我們需要提供以下介面的實作:
-
PublicKeyCredentialUserEntityRepository
-
UserCredentialRepository
6.1. PublicKeyCredentialUserEntityRepository
此服務管理PublicKeyCredentialUserEntity
實例,並將標準UserDetailsService
Service管理的使用者帳戶對應到使用者帳戶識別碼。該實體具有以下屬性:
-
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. 測試
雖然可以使用模擬請求來測試基於密碼的應用程序,但這些測試的價值有些有限。大多數故障場景都與客戶端問題有關,因此需要使用由自動化工具驅動的真實瀏覽器進行整合測試。
在這裡,我們將使用 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);
}
一旦我們獲得了authenticator
實例,我們就可以用它來模擬不同的場景,例如成功或不成功的登入、註冊等等。我們的現場測試經歷一個完整的週期,包括以下步驟:
- 使用使用者名稱/密碼憑證首次登入
- 密鑰註冊
- 登出
- 使用密鑰登入
8. 結論
在本教學中,我們展示如何在 Spring Boot Web 應用程式中使用 Passkeys,包括 Spring Security 設定和新增實際應用程式所需的金鑰持久性支援。
我們也提供如何使用 Selenium 測試應用程式的基本範例。與往常一樣,所有程式碼均可在 GitHub 上取得。