编辑此页面

使用 WebAuthn 进行安全

本指南演示了您的 Quarkus 应用程序如何使用 WebAuthn 身份验证而不是密码。

此技术被认为是预览版。

预览中,不保证向后兼容性和生态系统中的存在。具体的改进可能需要更改配置或 API,并且正在制定成为稳定的计划。欢迎在我们的邮件列表或我们的GitHub 问题跟踪器中提供反馈。

有关可能的完整状态列表,请查看我们的常见问题解答条目

先决条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

  • 已安装 JDK 17+ 并正确配置了 JAVA_HOME

  • Apache Maven 3.9.9

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

  • 支持 WebAuthn 或 PassKeys 的设备,或这些设备的模拟器

WebAuthn 简介

WebAuthn 是一种旨在取代密码的身份验证机制。简而言之,每次您编写用于注册新用户或登录用户的服务时,您都可以使用 WebAuthn 来代替要求密码,WebAuthn 将取代密码。

WebAuthn 用身份证明代替密码。实际上,用户不必发明密码、存储或记住密码,而是使用硬件令牌来生成专门为您的服务或网站设计的身份证明。这可以通过要求用户将拇指放在手机上,或者按下电脑上的 YubiKey 上的按钮来完成。

因此,当您注册用户时,您可以使用浏览器输入您的用户信息(用户名、您的姓名等……),而不是输入密码来验证您的身份,您可以单击一个按钮,该按钮将调用 WebAuthn 浏览器 API,该 API 会要求您执行某些操作(按下按钮、使用您的指纹)。然后,您的浏览器将生成一个身份证明,您可以将其发送到您的服务,而不是密码。

注册时的身份证明主要由一个公钥组成。实际上,其中有很多东西,但最有趣的是公钥。此公钥不存储在您的设备或浏览器上。它是专门为目标服务(与其 URI 相关联)生成的,并且是从硬件验证器派生的。因此,硬件验证器和目标服务的关联将始终派生相同的私钥和公钥对,这些密钥对都不会存储在任何地方。例如,您可以将您的 YubiKey 带到另一台电脑,它将继续为同一目标服务生成相同的私钥/公钥。

因此,当您注册时,您(主要)发送一个公钥而不是密码,并且该服务将该信息存储为您的新用户帐户的 WebAuthn 凭据,这就是以后验证您的身份的方式。

然后,当您需要登录该服务时,您可以按下登录表单上的一个按钮,而不是输入您的密码(请记住,密码不存在?),浏览器会要求您执行某些操作,然后它会将签名发送到您的服务,而不是密码。该签名需要从您的验证器硬件和目标服务派生的私钥,因此当您的服务收到该签名时,它可以验证它是否与您存储为凭据的公钥的签名相对应。

因此,概括地说:注册会发送生成的公钥而不是密码,而登录会发送该公钥的签名,从而允许您验证用户在注册时是否是其本人。

实际上,这有点复杂,因为在使用硬件验证器之前需要与服务器进行握手(要求一个质询和其他内容),因此总是需要对您的服务进行两次调用:一次在登录或注册之前,在调用硬件验证器之前,然后是正常的登录或注册。

此外,要存储的字段也比公钥多得多,但我们会帮助您解决这个问题。

如果您想知道与 PassKeys 的关系以及我们是否支持它:当然,是的,PassKeys 是您的验证器设备可以共享和同步其凭据的一种方式,然后您可以将这些凭据与我们的 WebAuthn 身份验证一起使用。

WebAuthn 规范要求使用 HTTPS 与服务器通信,但某些浏览器允许 localhost。如果您必须在开发模式下使用 HTTPS,您可以随时使用 quarkus-ngrok 扩展。

架构

在此示例中,我们构建了一个非常简单的微服务,该微服务提供四个端点

  • /api/public

  • /api/public/me

  • /api/users/me

  • /api/admin

/api/public 端点可以匿名访问。/api/public/me 端点可以匿名访问,如果存在用户名,则返回当前用户名,如果不存在,则返回 <not logged in>/api/admin 端点受到 RBAC(基于角色的访问控制)的保护,只有被授予 admin 角色的用户才能访问。在此端点,我们使用 @RolesAllowed 注释来声明性地强制执行访问约束。/api/users/me 端点也受到 RBAC(基于角色的访问控制)的保护,只有被授予 user 角色的用户才能访问。作为响应,它返回一个 JSON 文档,其中包含有关用户的详细信息。

解决方案

我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。

克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git,或下载 archive

该解决方案位于 security-webauthn-quickstart directory 中。

创建 Maven 项目

首先,我们需要一个新项目。使用以下命令创建一个新项目

CLI
quarkus create app org.acme:security-webauthn-quickstart \
    --extension='security-webauthn,jdbc-postgresql,rest,hibernate-orm-panache' \
    --no-code
cd security-webauthn-quickstart

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-webauthn-quickstart \
    -Dextensions='security-webauthn,jdbc-postgresql,rest,hibernate-orm-panache' \
    -DnoCode
cd security-webauthn-quickstart

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请用双引号将 -D 参数括起来,例如 "-DprojectArtifactId=security-webauthn-quickstart"

不要忘记添加选择的数据库连接器库。这里我们使用 PostgreSQL 作为身份存储。

此命令生成一个 Maven 项目,导入 security-webauthn 扩展,该扩展允许您使用 WebAuthn 对用户进行身份验证。

如果您已经配置了您的 Quarkus 项目,您可以通过在项目基本目录中运行以下命令将 security-webauthn 扩展添加到您的项目中

CLI
quarkus extension add security-webauthn
Maven
./mvnw quarkus:add-extension -Dextensions='security-webauthn'
Gradle
./gradlew addExtension --extensions='security-webauthn'

这会将以下内容添加到您的构建文件中

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-security-webauthn</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-security-webauthn")

编写应用程序

让我们从实现 /api/public 端点开始。从下面的源代码中可以看到,它只是一个普通的 Jakarta REST 资源

package org.acme.security.webauthn;

import java.security.Principal;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;

@Path("/api/public")
public class PublicResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String publicResource() {
        return "public";
   }

    @GET
    @Path("/me")
    @Produces(MediaType.TEXT_PLAIN)
    public String me(@Context SecurityContext securityContext) {
        Principal user = securityContext.getUserPrincipal();
        return user != null ? user.getName() : "<not logged in>";
    }
}

/api/admin 端点的源代码也非常简单。这里的主要区别在于我们使用 @RolesAllowed 注释来确保只有被授予 admin 角色的用户才能访问该端点

package org.acme.security.webauthn;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/api/admin")
public class AdminResource {

    @GET
    @RolesAllowed("admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String adminResource() {
         return "admin";
    }
}

最后,让我们考虑 /api/users/me 端点。从下面的源代码中可以看到,我们只信任具有 user 角色的用户。我们使用 SecurityContext 来访问当前经过身份验证的 Principal,并返回用户的姓名。此信息是从数据库加载的。

package org.acme.security.webauthn;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.SecurityContext;

@Path("/api/users")
public class UserResource {

    @GET
    @RolesAllowed("user")
    @Path("/me")
    public String me(@Context SecurityContext securityContext) {
        return securityContext.getUserPrincipal().getName();
    }
}

存储我们的 WebAuthn 凭据

我们现在可以使用两个实体来描述我们的 WebAuthn 凭据如何存储在我们的数据库中。请注意,我们简化了模型,以便每个用户只存储一个凭据(实际上,每个用户可以有多个 WebAuthn 凭据和其他数据,例如角色)

package org.acme.security.webauthn;

import java.util.List;
import java.util.UUID;

import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.security.webauthn.WebAuthnCredentialRecord.RequiredPersistedData;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;

@Entity
public class WebAuthnCredential extends PanacheEntityBase {

	@Id
    public String credentialId;

    public byte[] publicKey;
    public long publicKeyAlgorithm;
    public long counter;
    public UUID aaguid;

    // this is the owning side
    @OneToOne
    public User user;

    public WebAuthnCredential() {
    }

    public WebAuthnCredential(WebAuthnCredentialRecord credentialRecord, User user) {
        RequiredPersistedData requiredPersistedData =
            credentialRecord.getRequiredPersistedData();
        aaguid = requiredPersistedData.aaguid();
        counter = requiredPersistedData.counter();
        credentialId = requiredPersistedData.credentialId();
        publicKey = requiredPersistedData.publicKey();
        publicKeyAlgorithm = requiredPersistedData.publicKeyAlgorithm();
        this.user = user;
        user.webAuthnCredential = this;
    }

    public WebAuthnCredentialRecord toWebAuthnCredentialRecord() {
        return WebAuthnCredentialRecord
                .fromRequiredPersistedData(
                        new RequiredPersistedData(user.username, credentialId,
                                                  aaguid, publicKey,
                                                  publicKeyAlgorithm, counter));
    }

    public static List<WebAuthnCredential> findByUsername(String username) {
        return list("user.username", username);
    }

    public static WebAuthnCredential findByCredentialId(String credentialId) {
        return findById(credentialId);
    }
}

和我们的用户实体

package org.acme.security.webauthn;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;

@Table(name = "user_table")
@Entity
public class User extends PanacheEntity {

    @Column(unique = true)
    public String username;

    // non-owning side, so we can add more credentials later
    @OneToOne(mappedBy = "user")
    public WebAuthnCredential webAuthnCredential;

    public static User findByUsername(String username) {
        return User.find("username", username).firstResult();
    }
}

关于用户名和凭据 ID 的说明

用户名是唯一的,并且是给您的用户使用的。每个创建的 WebAuthn 凭据记录都有一个唯一的 ID。

您可以允许(如果您愿意,但您不必)您的用户拥有多个验证器设备,这意味着单个用户名可以映射到多个凭据 ID,所有这些 ID 都标识同一个用户。

将您的实体公开给 Quarkus WebAuthn

您需要定义一个 bean 来实现 WebAuthnUserProvider,以便允许 Quarkus WebAuthn 扩展加载和存储凭据。在这里,您可以告诉 Quarkus 如何将您的数据模型转换为 WebAuthn 安全模型

import java.util.Collections;
import java.util.List;
import java.util.Set;

import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.security.webauthn.WebAuthnUserProvider;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;

@Blocking
@ApplicationScoped
public class MyWebAuthnSetup implements WebAuthnUserProvider {

    @Transactional
    @Override
    public Uni<List<WebAuthnCredentialRecord>> findByUsername(String userId) {
        return Uni.createFrom().item(
            WebAuthnCredential.findByUsername(userId)
                              .stream()
                              .map(WebAuthnCredential::toWebAuthnCredentialRecord)
                              .toList());
    }

    @Transactional
    @Override
    public Uni<WebAuthnCredentialRecord> findByCredentialId(String credId) {
        WebAuthnCredential creds = WebAuthnCredential.findByCredentialId(credId);
        if(creds == null)
            return Uni.createFrom()
                      .failure(new RuntimeException("No such credential ID"));
        return Uni.createFrom().item(creds.toWebAuthnCredentialRecord());
    }

    @Transactional
    @Override
    public Uni<Void> store(WebAuthnCredentialRecord credentialRecord) {
        User newUser = new User();
        // We can only store one credential per username thanks to the unicity constraint
        // which will cause this transaction to fail and throw if the username already exists
        newUser.username = credentialRecord.getUsername();
        WebAuthnCredential credential = new WebAuthnCredential(credentialRecord, newUser);
        credential.persist();
        newUser.persist();
        return Uni.createFrom().voidItem();
    }

    @Transactional
    @Override
    public Uni<Void> update(String credentialId, long counter) {
        WebAuthnCredential credential =
            WebAuthnCredential.findByCredentialId(credentialId);
        credential.counter = counter;
        return Uni.createFrom().voidItem();
    }

    @Override
    public Set<String> getRoles(String userId) {
        if(userId.equals("admin")) {
            return Set.of("user", "admin");
        }
        return Collections.singleton("user");
    }
}

警告:在实现您自己的 WebAuthnUserProvider.store 方法时,请确保您永远不允许为已经存在的 username 创建新凭据。否则,您可能会允许第三方通过允许他们将自己的凭据添加到现有帐户来冒充现有用户。如果您想允许现有用户注册多个 WebAuthn 凭据,您必须确保在 WebAuthnUserProvider.store 中,用户当前以您想添加新凭据的同一 username 登录。在其他任何情况下,请确保从此方法返回一个失败的 Uni。在此特定示例中,这是通过用户名上的唯一性约束来检查的,如果用户已经存在,这将导致事务失败。

配置

因为我们想将登录和注册委托给默认的 Quarkus WebAuthn 端点,我们需要在配置中启用它们(src/main/resources/application.properties

quarkus.webauthn.enable-login-endpoint=true
quarkus.webauthn.enable-registration-endpoint=true

编写 HTML 应用程序

我们现在需要编写一个网页,其中包含指向我们所有 API 的链接,以及一种在 src/main/resources/META-INF/resources/index.html 中注册新用户、登录和注销的方式

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>Login</title>
    <script src="/q/webauthn/webauthn.js" type="text/javascript" charset="UTF-8"></script>
    <style>
     .container {
      display: grid;
      grid-template-columns: auto auto auto;
     }
     button, input {
      margin: 5px 0;
     }
     .item {
      padding: 20px;
     }
     nav > ul {
       list-style-type: none;
       margin: 0;
       padding: 0;
       overflow: hidden;
       background-color: #333;
     }

     nav > ul > li {
       float: left;
     }

     nav > ul > li > a {
       display: block;
       color: white;
       text-align: center;
       padding: 14px 16px;
       text-decoration: none;
     }

     nav > ul > li > a:hover {
       background-color: #111;
     }
    </style>
  </head>

  <body>
    <nav>
     <ul>
      <li><a href="/api/public">Public API</a></li>
      <li><a href="/api/users/me">User API</a></li>
      <li><a href="/api/admin">Admin API</a></li>
      <li><a href="/q/webauthn/logout">Logout</a></li>
     </ul>
    </nav>
    <div class="container">
     <div class="item">
      <h1>Status</h1>
      <div id="result"></div>
     </div>
     <div class="item">
      <h1>Login</h1>
      <p>
       <button id="login">Login</button>
      </p>
     </div>
     <div class="item">
      <h1>Register</h1>
      <p>
       <input id="usernameRegister" placeholder="User name"/><br/>
       <input id="firstName" placeholder="First name"/><br/>
       <input id="lastName" placeholder="Last name"/><br/>
       <button id="register">Register</button>
      </p>
     </div>
    </div>
    <script type="text/javascript">
      const webAuthn = new WebAuthn();

      const result = document.getElementById('result');

      fetch('/api/public/me')
        .then(response => response.text())
        .then(username => result.append("User: "+username));

      const loginButton = document.getElementById('login');

      loginButton.addEventListener("click", (e) => {
        result.replaceChildren();
        webAuthn.login()
          .then(x => fetch('/api/public/me'))
          .then(response => response.text())
          .then(username => {
            result.append("User: "+username);
          })
          .catch(err => {
            result.append("Login failed: "+err);
          });
        return false;
      });

      const registerButton = document.getElementById('register');

      registerButton.addEventListener("click", (e) => {
        var username = document.getElementById('usernameRegister').value;
        var firstName = document.getElementById('firstName').value;
        var lastName = document.getElementById('lastName').value;
        result.replaceChildren();
        webAuthn.register({ username: username, displayName: firstName + " " + lastName })
          .then(body => {
            result.append("User: "+username);
          })
          .catch(err => {
            result.append("Registration failed: "+err);
          });
        return false;
      });
    </script>
  </body>
</html>

尝试应用程序

该应用程序现在受到保护,并且身份由我们的数据库提供。

使用以下命令在开发模式下运行您的应用程序

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

这将启动一个 PostgreSQL 开发服务容器,并在您的浏览器中打开 https://:8080

最初,您将没有注册任何凭据,也没有当前用户

webauthn 1

当前用户显示在左侧,您可以使用顶部菜单尝试访问公共 API,这应该有效,而用户和管理员 API 将失败,并将您重定向到当前页面。

首先,通过在右侧的 Register 表单上输入用户名、名字和姓氏,然后按 Register 按钮来注册您的 WebAuthn 凭据

webauthn 2

您的浏览器会要求您激活您的 WebAuthn 验证器(您需要一个支持 WebAuthn 的浏览器和可能的设备,或者您可以使用这些设备的模拟器

webauthn 3

然后,您将登录,并且可以检查用户 API 现在是否可以访问

webauthn 4

在此阶段,您可以 Logout

webauthn 5

然后按 Login 按钮,您将登录

webauthn 4

只有您使用 admin 用户名注册时,管理员 API 才能访问。

WebAuthn 身份验证流程说明。

您想要处理用户和凭据创建的方式有两种操作模式

默认情况下,我们看到的解决方案会自动通过此扩展提供的开箱即用的 WebAuthn 端点处理用户和凭据创建以及登录。

在这种情况下,您只需提供一个 WebAuthnUserProvider 来告诉 Quarkus 如何查找、存储和更新您的凭据(请参阅将您的实体公开给 Quarkus WebAuthn)。

因此,在客户端上触发注册需要从服务器获取质询(请参阅获取注册质询),然后将质询返回到客户端,客户端使用该质询来创建新凭据并将其发送到服务器(请参阅触发注册),服务器将存储凭据并让您登录

webauthn register

稍后,在客户端上触发登录需要从服务器获取质询(请参阅获取登录质询),然后将质询返回到客户端,客户端使用该质询来获取所需的凭据并将凭据的证明发送到服务器(请参阅触发登录),服务器将更新凭据并让您登录

webauthn login

如果您想自定义注册和登录流程,您还可以通过您自己的逻辑自行处理用户和凭据创建以及登录(请参阅自行处理登录和注册端点)。

WebAuthn 端点

Quarkus WebAuthn 扩展提供了这些开箱即用的预定义 REST 端点

获取注册质询

GET /q/webauthn/register-options-challenge?username=<username>&displayName=<displayName>:设置并获取注册质询

查询参数

  • username 是用户名。必需。

  • displayName 是用户帐户的人工可读名称。可选。

这会导致为质询设置一个 cookie,稍后注册步骤将使用它。

响应
{
 "rp": {
   "name": "Quarkus server"
  },
 "user": {
   "id": "ryPi43NJSx6LFYNitrOvHg",
   "name": "FroMage",
   "displayName": "Mr Nice Guy"
  },
  "challenge": "6tkVLgYzp5yJz_MtnzCy6VRMkHuN4f4C-_hukRmsuQ_MQl7uxJweiqH8gaFkm_mEbKzlUbOabJM3nLbi08i1Uw",
  "pubKeyCredParams": [
    {
     "alg": -7,
     "type":"public-key"
    },
    {
     "alg": -257,
     "type": "public-key"
    }
  ],
  "authenticatorSelection": {
    "requireResidentKey": true,
    "residentKey": "required",
    "userVerification": "required"
  },
  "timeout": 300000,
  "attestation": "none",
  "extensions": {
   "txAuthSimple": ""
  }
 }

触发注册

POST /q/webauthn/register?username=<username>:触发注册

查询参数

  • username 是用户名。必需。

这使用注册质询设置的质询 cookie 并清除它。它还使用您的 WebAuthnUserProvider 来存储新凭据,并设置 会话 cookie 以让您登录。

只有在 quarkus.webauthn.enable-registration-endpoint 配置设置为 true 时,才会启用此功能。
请求
{
 "id": "boMwU-QwZ_RsToPTG3iC50g8-yiKbLc3A53tgWMhzbNEQAJIlbWgchmwbt5m0ssqQNR0IM_WxCmcfKWlEao7Fg",
 "rawId": "boMwU-QwZ_RsToPTG3iC50g8-yiKbLc3A53tgWMhzbNEQAJIlbWgchmwbt5m0ssqQNR0IM_WxCmcfKWlEao7Fg",
 "response": {
  "attestationObject": "<DATA>",
  "clientDataJSON":"<DATA>"
 },
 "type": "public-key"
}

这会返回 204,没有正文。

获取登录质询

GET /q/webauthn/login-options-challenge?username=<username>:设置并获取登录质询

查询参数

  • username 是用户名。在 可发现凭据(使用 PassKeys)的情况下,这是可选的。

这会导致为质询设置一个 cookie,稍后登录步骤将使用它。

响应
{
 "challenge": "RV4hqKHezkWSxpOICBkpx16yPJFGMZrkPlJP-Wp8w4rVl34VIzCT7AP0Q5Rv-3JCU3jwu-j3VlOgyNMDk2AqDg",
 "timeout": 300000,
 "userVerification": "required",
 "extensions": {
  "txAuthSimple": ""
 },
 "allowCredentials": [
  {
   "type": "public-key",
   "id": "boMwU-QwZ_RsToPTG3iC50g8-yiKbLc3A53tgWMhzbNEQAJIlbWgchmwbt5m0ssqQNR0IM_WxCmcfKWlEao7Fg",
   "transports": [
    "usb",
    "nfc",
    "ble",
    "internal"
   ]
  }
 ]
}

触发登录

这使用登录质询设置的质询 cookie 并清除它。它还使用您的 WebAuthnUserProvider 来查找和更新凭据,并设置 会话 cookie 以让您登录。

只有在 quarkus.webauthn.enable-login-endpoint 配置设置为 true 时,才会启用此功能。
请求
{
 "id": "boMwU-QwZ_RsToPTG3iC50g8-yiKbLc3A53tgWMhzbNEQAJIlbWgchmwbt5m0ssqQNR0IM_WxCmcfKWlEao7Fg",
 "rawId": "boMwU-QwZ_RsToPTG3iC50g8-yiKbLc3A53tgWMhzbNEQAJIlbWgchmwbt5m0ssqQNR0IM_WxCmcfKWlEao7Fg",
 "response": {
  "clientDataJSON": "<DATA>",
  "authenticatorData": "<DATA>",
  "signature": "<DATA>",
  "userHandle": ""
 },
 "type": "public-key"
}

这会返回 204,没有正文。

注销

GET /q/webauthn/logout:让您注销

这将清除 会话 cookie 以让您注销。

这会返回 302 重定向到您应用程序的根 URI。

GET /.well-known/webauthn:检索 相关来源的列表。

默认情况下,这是 Quarkus 部署的当前来源,但可以进行配置

响应
{
  "origins": [
    "https://shopping.com",
    "https://shopping.co.uk",
    "https://shopping.co.jp",
    "https://shopping.ie",
    "https://shopping.ca",
    "https://shopping.net",
    "https://shopping.org",
    "https://shopping.github.io"
  ]
}

WebAuthn JavaScript 库

因为需要在浏览器中设置 WebAuthn 的 JavaScript 代码非常多,所以 Quarkus WebAuthn 扩展附带了一个 JavaScript 库来帮助您与 WebAuthn 端点进行通信,该库位于 /q/webauthn/webauthn.js。您可以像这样设置它

<script src="/q/webauthn/webauthn.js" type="text/javascript" charset="UTF-8"></script>
<script type="text/javascript">
  // keep the default /q/webauthn endpoints
  const webAuthn = new WebAuthn();
  // use the webAuthn APIs here
</script>

或者,如果您需要自定义端点

<script src="/q/webauthn/webauthn.js" type="text/javascript" charset="UTF-8"></script>
<script type="text/javascript">
  // configure where our endpoints are
  const webAuthn = new WebAuthn({
    registerOptionsChallengePath: '/q/webauthn/register-options-challenge',
    loginOptionsChallengePath: '/q/webauthn/login-options-challenge',
    registerPath: '/q/webauthn/register',
    loginPath: '/q/webauthn/login'
  });
  // use the webAuthn APIs here
</script>

CSRF 注意事项

如果您使用 Quarkus 提供的端点,它们将不受 xdoc:security-csrf-prevention.adoc[CSRF] 的保护,但是如果您定义自己的端点并使用此 JavaScript 库来访问它们,您将需要通过标头配置 CSRF

<script src="/q/webauthn/webauthn.js" type="text/javascript" charset="UTF-8"></script>
<script type="text/javascript">
  // configure where our endpoints are
  const webAuthn = new WebAuthn({
    'csrf': {
      'header': '{inject:csrf.headerName}',
      'value': '{inject:csrf.token}'
	}
  });
  // use the webAuthn APIs here
</script>

调用注册

webAuthn.register 方法调用注册质询端点,然后调用验证器并调用该注册的注册端点,并返回一个 Promise 对象

webAuthn.register({ username: username, displayName: firstName + " " + lastName })
  .then(body => {
    // do something now that the user is registered
  })
  .catch(err => {
    // registration failed
  });

调用登录

webAuthn.login 方法调用登录质询端点,然后调用验证器并调用该登录的登录端点,并返回一个 Promise 对象

webAuthn.login({ username: username }) (1)
  .then(body => {
    // do something now that the user is logged in
  })
  .catch(err => {
    // login failed
  });
1 可发现凭据(使用 PassKeys)的情况下,用户名是可选的

仅调用注册质询和验证器

webAuthn.registerClientSteps 方法调用注册质询端点,然后调用验证器并返回一个 Promise 对象,该对象包含一个适合发送到注册端点的 JSON 对象。例如,您可以使用该 JSON 对象将凭据存储在隐藏的表单 input 元素中,并将其作为常规 HTML 表单的一部分发送

webAuthn.registerClientSteps({ username: username, displayName: firstName + " " + lastName })
  .then(body => {
    // store the registration JSON in form elements
    document.getElementById('webAuthnId').value = body.id;
    document.getElementById('webAuthnRawId').value = body.rawId;
    document.getElementById('webAuthnResponseAttestationObject').value = body.response.attestationObject;
    document.getElementById('webAuthnResponseClientDataJSON').value = body.response.clientDataJSON;
    document.getElementById('webAuthnType').value = body.type;
  })
  .catch(err => {
    // registration failed
  });

仅调用登录质询和验证器

webAuthn.loginClientSteps 方法调用登录质询端点,然后调用验证器并返回一个 Promise 对象,该对象包含一个适合发送到登录端点的 JSON 对象。例如,您可以使用该 JSON 对象将凭据存储在隐藏的表单 input 元素中,并将其作为常规 HTML 表单的一部分发送

webAuthn.loginClientSteps({ username: username }) (1)
  .then(body => {
    // store the login JSON in form elements
    document.getElementById('webAuthnId').value = body.id;
    document.getElementById('webAuthnRawId').value = body.rawId;
    document.getElementById('webAuthnResponseClientDataJSON').value = body.response.clientDataJSON;
    document.getElementById('webAuthnResponseAuthenticatorData').value = body.response.authenticatorData;
    document.getElementById('webAuthnResponseSignature').value = body.response.signature;
    document.getElementById('webAuthnResponseUserHandle').value = body.response.userHandle;
    document.getElementById('webAuthnType').value = body.type;
  })
  .catch(err => {
    // login failed
  });
1 可发现凭据(使用 PassKeys)的情况下,用户名是可选的

自行处理登录和注册端点

有时,您可能希望请求比用户名更多的数据才能注册用户,或者您希望使用自定义验证来处理登录和注册,因此默认的 WebAuthn 登录和注册端点是不够的。

在这种情况下,您可以使用 JavaScript 库中的 WebAuthn.loginClientStepsWebAuthn.registerClientSteps 方法,将验证器数据存储在隐藏的表单元素中,并将其作为表单有效负载的一部分发送到服务器到您的自定义登录或注册端点。

如果您将它们存储在表单输入元素中,那么您可以使用 WebAuthnLoginResponseWebAuthnRegistrationResponse 类,将它们标记为 @BeanParam,然后使用 WebAuthnSecurity.loginWebAuthnSecurity.register 方法来替换 /q/webauthn/login/q/webauthn/register 端点。

在大多数情况下,您可以继续使用 /q/webauthn/login-options-challenge/q/webauthn/register-options-challenge 质询启动端点,因为这不需要自定义逻辑。

在这种情况下,注册流程有点不同,因为您将编写自己的注册端点,该端点将处理凭据的存储和会话 cookie 的设置

webauthn custom register

同样,登录流程有点不同,因为您将编写自己的登录端点,该端点将处理凭据的更新和会话 cookie 的设置

webauthn custom login

如果您在端点中自行处理用户和凭据创建以及登录,您只需要在您的 WebAuthnUserProvider 中提供实体的只读视图,因此您可以跳过 storeupdate 方法

package org.acme.security.webauthn;

import java.util.List;

import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.security.webauthn.WebAuthnUserProvider;
import io.smallrye.common.annotation.Blocking;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import model.WebAuthnCredential;

@Blocking
@ApplicationScoped
public class MyWebAuthnSetup implements WebAuthnUserProvider {

    @Transactional
    @Override
    public Uni<List<WebAuthnCredentialRecord>> findByUsername(String username) {
        return Uni.createFrom().item(
            WebAuthnCredential.findByUsername(username)
                              .stream()
                              .map(WebAuthnCredential::toWebAuthnCredentialRecord)
                              .toList());
    }

    @Transactional
    @Override
    public Uni<WebAuthnCredentialRecord> findByCredentialId(String credentialId) {
        WebAuthnCredential creds = WebAuthnCredential.findByCredentialId(credentialId);
        if(creds == null)
            return Uni.createFrom()
                      .failure(new RuntimeException("No such credential ID"));
        return Uni.createFrom().item(creds.toWebAuthnCredentialRecord());
    }

    @Override
    public Set<String> getRoles(String userId) {
        if(userId.equals("admin")) {
            return Set.of("user", "admin");
        }
        return Collections.singleton("user");
    }
}
在设置您自己的登录和注册端点时,您不需要启用默认端点,因此您可以删除 quarkus.webauthn.enable-login-endpointquarkus.webauthn.enable-registration-endpoint 配置。

值得庆幸的是,您可以使用 WebAuthnSecurity bean 来处理您的注册和登录端点的 WebAuthn 特定部分,并将精力集中在您自己的逻辑上

package org.acme.security.webauthn;

import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;

import org.jboss.resteasy.reactive.RestForm;

import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.security.webauthn.WebAuthnLoginResponse;
import io.quarkus.security.webauthn.WebAuthnRegisterResponse;
import io.quarkus.security.webauthn.WebAuthnSecurity;
import io.vertx.ext.web.RoutingContext;

@Path("")
public class LoginResource {

    @Inject
    WebAuthnSecurity webAuthnSecurity;

    // Provide an alternative implementation of the /q/webauthn/login endpoint
    @Path("/login")
    @POST
    @Transactional
    public Response login(@BeanParam WebAuthnLoginResponse webAuthnResponse,
                          RoutingContext ctx) {
        // Input validation
        if(!webAuthnResponse.isSet() || !webAuthnResponse.isValid()) {
            return Response.status(Status.BAD_REQUEST).build();
        }

        try {
            WebAuthnCredentialRecord credentialRecord = this.webAuthnSecurity.login(webAuthnResponse, ctx).await().indefinitely();
            User user = User.findByUsername(credentialRecord.getUsername());
            if(user == null) {
                // Invalid user
                return Response.status(Status.BAD_REQUEST).build();
            }
            // bump the auth counter
            user.webAuthnCredential.counter = credentialRecord.getCounter();
            // make a login cookie
            this.webAuthnSecurity.rememberUser(credentialRecord.getUsername(), ctx);
            return Response.ok().build();
        } catch (Exception exception) {
            // handle login failure - make a proper error response
            return Response.status(Status.BAD_REQUEST).build();
        }
    }

    // Provide an alternative implementation of the /q/webauthn/register endpoint
    @Path("/register")
    @POST
    @Transactional
    public Response register(@RestForm String username,
                             @BeanParam WebAuthnRegisterResponse webAuthnResponse,
                             RoutingContext ctx) {
        // Input validation
        if(username == null || username.isEmpty()
           || !webAuthnResponse.isSet() || !webAuthnResponse.isValid()) {
            return Response.status(Status.BAD_REQUEST).build();
        }

        User user = User.findByUsername(username);
        if(user != null) {
            // Duplicate user
            return Response.status(Status.BAD_REQUEST).build();
        }
        try {
            // store the user
            WebAuthnCredentialRecord credentialRecord =
                webAuthnSecurity.register(username, webAuthnResponse, ctx).await().indefinitely();
            User newUser = new User();
            newUser.username = credentialRecord.getUsername();
            WebAuthnCredential credential =
                new WebAuthnCredential(credentialRecord, newUser);
            credential.persist();
            newUser.persist();
            // make a login cookie
            this.webAuthnSecurity.rememberUser(newUser.username, ctx);
            return Response.ok().build();
        } catch (Exception ignored) {
            // handle login failure
            // make a proper error response
            return Response.status(Status.BAD_REQUEST).build();
        }
    }
}
WebAuthnSecurity 方法不设置或读取 会话 cookie,因此您必须自行处理它,但这允许您使用其他方式来存储用户,例如 JWT。如果您想手动设置登录 cookie,您可以使用同一 WebAuthnSecurity 类上的 WebAuthnSecurity.rememberUserWebAuthnSecurity.logout 方法。

阻塞版本

如果您使用阻塞数据访问数据库,您可以安全地阻塞 WebAuthnSecurity 方法,使用 .await().indefinitely(),因为 registerlogin 方法中没有任何异步操作,除了通过您的 WebAuthnUserProvider 进行的数据访问。

您必须在您的 WebAuthnUserProvider 类上添加 @Blocking 注释,以便 Quarkus WebAuthn 端点将这些调用推迟到工作线程池。

虚拟线程版本

如果您使用阻塞数据访问数据库,您可以安全地阻塞 WebAuthnSecurity 方法,使用 .await().indefinitely(),因为 registerlogin 方法中没有任何异步操作,除了通过您的 WebAuthnUserProvider 进行的数据访问。

您必须在您的 WebAuthnUserProvider 类上添加 @RunOnVirtualThread 注释,以便告诉 Quarkus WebAuthn 端点将这些调用推迟到工作线程池。

测试 WebAuthn

测试 WebAuthn 可能很复杂,因为通常您需要一个硬件令牌,这就是我们创建 quarkus-test-security-webauthn 帮助程序库的原因

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-security-webauthn</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-security-webauthn")

使用此库,您可以使用 WebAuthnHardware 来模拟验证器令牌,以及 WebAuthnEndpointHelper 帮助程序方法来调用 WebAuthn 端点,甚至填写自定义端点的表单数据

package org.acme.security.webauthn.test;

import static io.restassured.RestAssured.given;

import java.net.URL;
import java.util.function.Consumer;

import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.webauthn.WebAuthnEndpointHelper;
import io.quarkus.test.security.webauthn.WebAuthnHardware;
import io.restassured.RestAssured;
import io.restassured.filter.Filter;
import io.restassured.filter.cookie.CookieFilter;
import io.restassured.specification.RequestSpecification;
import io.vertx.core.json.JsonObject;

@QuarkusTest
public class WebAuthnResourceTest {

    enum User {
        USER, ADMIN;
    }
    enum Endpoint {
        DEFAULT, MANUAL;
    }

    @TestHTTPResource
    URL url;

    @Test
    public void testWebAuthnUser() {
        testWebAuthn("FroMage", User.USER, Endpoint.DEFAULT);
        testWebAuthn("scooby", User.USER, Endpoint.MANUAL);
    }

    @Test
    public void testWebAuthnAdmin() {
        testWebAuthn("admin", User.ADMIN, Endpoint.DEFAULT);
    }

    private void testWebAuthn(String username, User user, Endpoint endpoint) {
        Filter cookieFilter = new CookieFilter();
        WebAuthnHardware token = new WebAuthnHardware(url);

        verifyLoggedOut(cookieFilter);

        // two-step registration
        String challenge = WebAuthnEndpointHelper.obtainRegistrationChallenge(username, cookieFilter);
        JsonObject registrationJson = token.makeRegistrationJson(challenge);
        if(endpoint == Endpoint.DEFAULT)
            WebAuthnEndpointHelper.invokeRegistration(username, registrationJson, cookieFilter);
        else {
            invokeCustomEndpoint("/register", cookieFilter, request -> {
                WebAuthnEndpointHelper.addWebAuthnRegistrationFormParameters(request, registrationJson);
                request.formParam("username", username);
            });
        }

        // verify that we can access logged-in endpoints
        verifyLoggedIn(cookieFilter, username, user);

        // logout
        WebAuthnEndpointHelper.invokeLogout(cookieFilter);

        verifyLoggedOut(cookieFilter);

        // two-step login
        challenge = WebAuthnEndpointHelper.obtainLoginChallenge(null, cookieFilter);
        JsonObject loginJson = token.makeLoginJson(challenge);
        if(endpoint == Endpoint.DEFAULT)
            WebAuthnEndpointHelper.invokeLogin(loginJson, cookieFilter);
        else {
            invokeCustomEndpoint("/login", cookieFilter, request -> {
                WebAuthnEndpointHelper.addWebAuthnLoginFormParameters(request, loginJson);
            });
        }

        // verify that we can access logged-in endpoints
        verifyLoggedIn(cookieFilter, username, user);

        // logout
        WebAuthnEndpointHelper.invokeLogout(cookieFilter);

        verifyLoggedOut(cookieFilter);
    }

    private void invokeCustomEndpoint(String uri, Filter cookieFilter, Consumer<RequestSpecification> requestCustomiser) {
        RequestSpecification request = given()
        .when();
        requestCustomiser.accept(request);
        request
        .filter(cookieFilter)
        .redirects().follow(false)
        .log().ifValidationFails()
        .post(uri)
        .then()
        .statusCode(200)
        .log().ifValidationFails()
        .cookie(WebAuthnEndpointHelper.getChallengeCookie(), Matchers.is(""))
        .cookie(WebAuthnEndpointHelper.getMainCookie(), Matchers.notNullValue());
    }

    private void verifyLoggedIn(Filter cookieFilter, String username, User user) {
        // public API still good
        RestAssured.given().filter(cookieFilter)
        .when()
        .get("/api/public")
        .then()
        .statusCode(200)
        .body(Matchers.is("public"));
        // public API user name
        RestAssured.given().filter(cookieFilter)
        .when()
        .get("/api/public/me")
        .then()
        .statusCode(200)
        .body(Matchers.is(username));

        // user API accessible
        RestAssured.given().filter(cookieFilter)
        .when()
        .get("/api/users/me")
        .then()
        .statusCode(200)
        .body(Matchers.is(username));

        // admin API?
        if(user == User.ADMIN) {
            RestAssured.given().filter(cookieFilter)
            .when()
            .get("/api/admin")
            .then()
            .statusCode(200)
            .body(Matchers.is("admin"));
        } else {
            RestAssured.given().filter(cookieFilter)
            .when()
            .get("/api/admin")
            .then()
            .statusCode(403);
        }
    }

    private void verifyLoggedOut(Filter cookieFilter) {
        // public API still good
        RestAssured.given().filter(cookieFilter)
        .when()
        .get("/api/public")
        .then()
        .statusCode(200)
        .body(Matchers.is("public"));
        // public API user name
        RestAssured.given().filter(cookieFilter)
        .when()
        .get("/api/public/me")
        .then()
        .statusCode(200)
        .body(Matchers.is("<not logged in>"));

        // user API not accessible
        RestAssured.given()
        .filter(cookieFilter)
        .redirects().follow(false)
        .when()
        .get("/api/users/me")
        .then()
        .statusCode(302)
        .header("Location", Matchers.is("https://:8081/"));

        // admin API not accessible
        RestAssured.given()
        .filter(cookieFilter)
        .redirects().follow(false)
        .when()
        .get("/api/admin")
        .then()
        .statusCode(302)
        .header("Location", Matchers.is("https://:8081/"));
    }
}

对于此测试,由于我们正在测试提供的回调端点(它在其 WebAuthnUserProvider 中更新用户)和手动 LoginResource 端点(它手动处理用户),我们需要使用一个不更新 scooby 用户的 WebAuthnUserProvider 来覆盖 WebAuthnUserProvider

package org.acme.security.webauthn.test;

import org.acme.security.webauthn.MyWebAuthnSetup;
import org.acme.security.webauthn.WebAuthnCredential;

import io.quarkus.security.webauthn.WebAuthnCredentialRecord;
import io.quarkus.test.Mock;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;

@Mock
@ApplicationScoped
public class TestUserProvider extends MyWebAuthnSetup {
    @Transactional
    @Override
    public Uni<Void> store(WebAuthnCredentialRecord credentialRecord) {
        // this user is handled in the LoginResource endpoint manually
        if (credentialRecord.getUsername().equals("scooby")) {
            return Uni.createFrom().voidItem();
        }
        return super.store(credentialRecord);
    }

    @Transactional
    @Override
    public Uni<Void> update(String credentialId, long counter) {
        WebAuthnCredential credential = WebAuthnCredential.findByCredentialId(credentialId);
        // this user is handled in the LoginResource endpoint manually
        if (credential.user.username.equals("scooby")) {
        	return Uni.createFrom().voidItem();
        }
        return super.update(credentialId, counter);
    }
}

配置参考

可以使用 quarkus.http.auth.session.encryption-key 配置选项设置安全加密密钥,如安全指南中所述。

构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖

配置属性

类型

默认

是否启用 WebAuthn 扩展。

环境变量:QUARKUS_WEBAUTHN_ENABLED

显示更多

布尔值

true

应用程序的来源。来源基本上是协议、主机和端口。如果您在您的应用程序位于 https://example.com/login 时调用 WebAuthn API,那么来源将是 https://example.com。如果您从 https://:2823/test 调用,那么来源将是 https://:2823。请注意,除非是在 localhost 上,否则 WebAuthn API 将无法在通过 HTTP 加载的页面上工作,localhost 被认为是安全上下文。如果未指定,则默认为此应用程序部署到的 URI。如果您想允许多个来源,则允许有多个值。请参阅 https://w3c.github.io/webauthn/#sctn-related-origins

环境变量:QUARKUS_WEBAUTHN_ORIGINS

显示更多

字符串列表

此应用程序部署到的 URI

应用程序允许的验证器传输。验证器可以通过多种传输与用户 Web 浏览器交互。应用程序可能出于额外的安全加固原因而限制传输协议。默认情况下,应允许所有传输。如果您的应用程序将被手机用户使用,您可能只想限制只允许 INTERNAL 验证器。允许的值为

  • USB - USB 连接的验证器(例如:Yubikey 的)

  • NFC - NFC 连接的验证器(例如:Yubikey 的)

  • BLE - 蓝牙 LE 连接的验证器

  • INTERNAL - 硬件安全芯片(例如:Intel TPM2.0)

环境变量:QUARKUS_WEBAUTHN_TRANSPORTS

显示更多

usbnfcblehybridinternal 列表

USB,NFC,BLE,INTERNAL

id(或您的服务器的域名,从 origins 的第一个条目获得,或者从服务此请求的位置查看)

环境变量:QUARKUS_WEBAUTHN_RELYING_PARTY_ID

显示更多

字符串

第一个允许的来源的主机名,或者此应用程序部署到的主机

您的服务器的用户友好名称

环境变量:QUARKUS_WEBAUTHN_RELYING_PARTY_NAME

显示更多

字符串

Quarkus 服务器

允许的验证器附件的类型。验证器可以通过两种形式连接到您的设备

  • PLATFORM - 验证器内置在您的设备中(例如:安全芯片)

  • CROSS_PLATFORM - 验证器可以在设备之间漫游(例如:USB 验证器)

出于安全原因,您的应用程序可以选择限制为特定的附件模式。如果省略,则允许任何模式。

环境变量:QUARKUS_WEBAUTHN_AUTHENTICATOR_ATTACHMENT

显示更多

platformcross-platform

加载 FIDO 元数据以进行验证。请参阅 https://fidoalliance.org/metadata/。仅适用于与 Attestation.NONE 不同的证明。

环境变量:QUARKUS_WEBAUTHN_LOAD_METADATA

显示更多

布尔值

false

所需的常驻密钥。常驻(私钥)密钥是无法离开您的验证器设备的密钥,这意味着您无法重用该验证器登录到第二台计算机。

环境变量:QUARKUS_WEBAUTHN_RESIDENT_KEY

显示更多

discouragedpreferredrequired

REQUIRED

用户验证要求。Webauthn 应用程序可以选择 REQUIRED 验证来断言用户在身份验证仪式期间存在,但在某些情况下,应用程序可能希望减少与用户的交互,即:防止使用弹出窗口。有效值为

  • REQUIRED - 用户必须始终与浏览器交互

  • PREFERRED - 用户应始终与浏览器交互

  • DISCOURAGED - 用户应避免与浏览器交互

环境变量:QUARKUS_WEBAUTHN_USER_VERIFICATION

显示更多

requiredpreferreddiscouraged

REQUIRED

用户存在要求。

环境变量:QUARKUS_WEBAUTHN_USER_PRESENCE_REQUIRED

显示更多

布尔值

true

非负用户验证超时。身份验证必须在超时时间内发生,这将防止用户浏览器被需要用户验证的弹出窗口阻止,并且整个仪式必须在超时时间内完成。超时后,任何先前颁发的质询都会自动失效。

环境变量:QUARKUS_WEBAUTHN_TIMEOUT

显示更多

Duration 

5m

设备证明首选项。在注册期间,应用程序可能想要证明设备。证明是对验证器硬件的加密验证。证明意味着用户的隐私可能会被暴露,并且浏览器可能会代表用户覆盖所需的配置。有效值为

  • NONE - 没有证明数据与注册一起发送

  • INDIRECT - 证明数据与注册一起发送,由受信任的 CA 产生匿名数据

  • DIRECT - 证明数据与注册一起发送

  • ENTERPRISE - 没有证明数据与注册一起发送。设备 AAGUID 保持不变返回。

环境变量:QUARKUS_WEBAUTHN_ATTESTATION

显示更多

noneindirectdirectenterprise

NONE

按首选项顺序排列的允许的公钥凭据算法。Webauthn 强制所有验证器必须至少支持以下 2 种算法:ES256RS256。应用程序可能需要更强的密钥和算法,例如:ES512EdDSA。请注意,使用更强的算法,例如:EdDSA 可能需要 Java 15 或实现这些算法的加密 JCE 提供程序。请参阅 https://www.w3.org/TR/webauthn-1/#dictdef-publickeycredentialparameters

环境变量:QUARKUS_WEBAUTHN_PUBLIC_KEY_CREDENTIAL_PARAMETERS

显示更多

es256es384es512ps256ps384ps512es256krs256rs384rs512rs1ed-dsa 列表

ES256,RS256

应用程序和浏览器之间交换的质询的长度。质询必须至少为 32 字节。

环境变量:QUARKUS_WEBAUTHN_CHALLENGE_LENGTH

显示更多

整数

64

登录页面

环境变量:QUARKUS_WEBAUTHN_LOGIN_PAGE

显示更多

字符串

/login.html

非活动(空闲)超时。当达到非活动超时时,cookie 不会续订,并且会强制执行新的登录。

环境变量:QUARKUS_WEBAUTHN_SESSION_TIMEOUT

显示更多

Duration 

PT30M

Cookie 在被具有更新超时时间的新 cookie 替换之前可以存在多久,也称为“renewal-timeout”。请注意,较小的值会导致服务器负载略微增加(因为会更频繁地生成新的加密 cookie);但是,较大的值会影响不活动超时,因为超时是在生成 cookie 时设置的。例如,如果此值设置为 10 分钟,并且不活动超时为 30 分钟,则如果用户上次请求的时间是 cookie 已经存在 9 分钟时,则实际超时将在上次请求后 21 分钟发生,因为超时仅在生成新 cookie 时刷新。也就是说,服务器端不跟踪超时;时间戳被编码和加密在 cookie 本身中,并且每次请求都会被解密和解析。

环境变量:QUARKUS_WEBAUTHN_NEW_COOKIE_INTERVAL

显示更多

Duration 

PT1M

用于存储持久会话的 cookie。

环境变量:QUARKUS_WEBAUTHN_COOKIE_NAME

显示更多

字符串

quarkus-credential

用于在登录/注册期间存储 challenge 数据的 cookie

环境变量:QUARKUS_WEBAUTHN_CHALLENGE_COOKIE_NAME

显示更多

字符串

_quarkus_webauthn_challenge

会话 cookie 的 SameSite 属性。

环境变量:QUARKUS_WEBAUTHN_COOKIE_SAME_SITE

显示更多

strict, lax, none

strict

会话 cookie 的 cookie 路径。

环境变量:QUARKUS_WEBAUTHN_COOKIE_PATH

显示更多

字符串

/

会话 cookie 的 Max-Age 属性。 这是浏览器将保留 cookie 的时间。 默认值为空,这意味着 cookie 将保留到浏览器关闭为止。

环境变量:QUARKUS_WEBAUTHN_COOKIE_MAX_AGE

显示更多

Duration 

如果您想启用 /q/webauthn/register 的默认注册端点,请设置为 true,在这种情况下,您还应该实现 WebAuthnUserProvider.store 方法。

环境变量:QUARKUS_WEBAUTHN_ENABLE_REGISTRATION_ENDPOINT

显示更多

布尔值

false

如果您想启用 /q/webauthn/login 的默认登录端点,请设置为 true,在这种情况下,您还应该实现 WebAuthnUserProvider.update 方法。

环境变量:QUARKUS_WEBAUTHN_ENABLE_LOGIN_ENDPOINT

显示更多

布尔值

false

关于 Duration 格式

要编写持续时间值,请使用标准 java.time.Duration 格式。 有关更多信息,请参见 Duration#parse() Java API 文档

您还可以使用简化的格式,以数字开头

  • 如果该值仅为一个数字,则表示以秒为单位的时间。

  • 如果该值是一个数字后跟 ms,则表示以毫秒为单位的时间。

在其他情况下,简化格式将被转换为 java.time.Duration 格式以进行解析

  • 如果该值是一个数字后跟 hms,则在其前面加上 PT

  • 如果该值是一个数字后跟 d,则在其前面加上 P

相关内容