1. 简介

登录表单长期以来一直是需要身份验证的Web服务的标准功能。然而随着安全问题的日益突出,传统文本密码的弱点逐渐暴露:它们可能被猜测、拦截或泄露,导致安全事件并造成财务和声誉损失。

之前的替代方案(如mTLS、安全卡等)试图解决这个问题,但带来了糟糕的用户体验和额外成本。

本教程将探讨Passkeys(又称WebAuthn),这是一种提供密码安全替代方案的标准。我们将演示如何快速为Spring Boot应用添加这种身份验证机制支持。

2. 什么是Passkey?

Passkeys或WebAuthn是W3C联盟定义的标准API,允许Web浏览器应用管理公钥并将其注册给特定服务提供商。

典型注册流程如下:

  1. 用户在服务上创建新账户,初始凭证通常是熟悉的用户名/密码
  2. 注册后,用户进入个人资料页面选择"创建passkey"
  3. 系统显示passkey注册表单
  4. 用户填写必要信息(如密钥标签,便于后续选择)并提交
  5. 系统将passkey保存到数据库并与用户账户关联,同时密钥的私钥部分会存储在用户设备上
  6. passkey注册完成

密钥注册完成后,用户可使用存储的passkey访问服务。根据浏览器和设备的安全配置,登录可能需要指纹扫描、解锁手机或类似操作。

Passkey由两部分组成:浏览器发送给服务提供商的公钥,以及保留在本地设备上的私钥。

此外,客户端API实现确保特定passkey只能用于注册它的同一站点

3. 在Spring Boot应用中添加Passkeys

创建一个简单的Spring Boot应用来测试passkeys。我们的应用将只有一个欢迎页面,显示当前用户名和passkey注册页面的链接

首先添加必要依赖:

<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通过webauthn()方法原生支持passkeys。

@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:站点外部URL(必须使用HTTPS,localhost除外)
  • rpId:应用标识符(必须是匹配allowedOrigin主机名部分的有效域名)
  • rpName:浏览器在注册/登录过程中可能使用的友好名称

但此配置有个关键缺陷:应用重启后注册的密钥会丢失。这是因为Spring Security默认使用内存凭证存储,不适合生产环境。

稍后我们将解决这个问题

5. Passkey功能演示

配置好passkey后,让我们快速体验应用功能。使用mvn spring-boot:run或IDE启动应用后,浏览器访问http://localhost:8080

带passkey的登录表单

Spring应用的默认登录页面现在会包含"使用passkey登录"按钮。由于尚未注册密钥,需使用application.yaml中配置的用户名/密码(alice/changeit)登录:

欢迎页面

如预期所示,我们以Alice身份登录。点击"Register PassKey"链接进入注册页面:

Passkey注册页面

只需提供标签(如baeldung-demo)并点击"Register"按钮。后续操作取决于设备类型(桌面/移动/平板)和操作系统(Windows/Linux/Mac/Android),最终会新增一个密钥

Passkey注册成功

例如在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:账户名的备用版本,用于显示

注意当前实现假设nameid在特定认证域内唯一。

通常,此表中的条目与UserDetailsService管理的账户是1:1关系。

实现代码使用Spring Data JDBC将这些字段存储到PASSKEY_USERS表。

6.2. UserCredentialRepository

管理CredentialRecord实例,存储注册过程中从浏览器接收的实际公钥。该实体包含W3C文档规定的所有推荐属性,以及一些额外属性:

  • userEntityUserId:拥有此凭证的PublicKeyCredentialUserEntity标识符
  • label:用户定义的凭证标签(注册时分配)
  • lastUsed:凭证最后使用日期
  • created:凭证创建日期

注意CredentialRecordPublicKeyCredentialUserEntity是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);
}

获取authenticator实例后,可模拟不同场景(成功/失败登录、注册等)。我们的实时测试包含完整流程:

  1. 使用用户名/密码初始登录
  2. 注册passkey
  3. 退出登录
  4. 使用passkey登录

8. 结论

本教程展示了如何在Spring Boot Web应用中使用Passkeys,包括Spring Security设置和生产环境所需的密钥持久化支持。通过合理配置和存储实现,可以显著提升应用的安全性,同时改善用户体验。


原始标题:Integrating Passkeys into Spring Security | Baeldung