Vert.x FIDO2 webauthn 用于 Web 应用程序

本指南将向您展示如何构建并保护一个简单的 符合 FIDO2 标准(通常称为 Web Authentication API)的 vert.x Web 应用程序。Web Authentication API(也称为 WebAuthn)是由 W3C 和 FIDO 联盟在 Google、Mozilla、Microsoft、Yubico 和其他公司的参与下编写的规范。该 API 允许服务器使用公钥加密而不是密码来注册和认证用户。

您将构建什么

通过本指南,您将拥有一个无需密码即可执行身份验证的运行中的应用程序。这将通过 WebAuthn 实现,WebAuthn 是一个新的 W3C 全球标准,用于 Web 上的安全身份验证,并受所有主流浏览器和平台支持。

您需要什么

  • 文本编辑器或 IDE

  • Java 8 或更高版本(建议使用 11 或 >=15 获取额外的安全算法)

  • 互联网连接

创建项目

前往 start.vertx.io 并 创建一个项目,包含以下依赖项:

  • Vert.x Web

  • Webauthn Auth

project

安全优先

Web Authentication API 是一个安全的 API,供应商决定遵循最佳实践。虽然您可以在不使用 SSL 的情况下构建服务器,但现代 Web 浏览器不会连接或允许将 WebAuthn API 用于不使用 SSL 的服务器,即使是在开发过程中也是如此。

在我们开始使用 WebAuthn 之前,我们需要确保即使是我们的开发应用程序也已启用 SSL。为此,我们需要创建一个有效的 SSL 证书。请注意,自签名证书仍然允许使用

要为您的 IP 地址创建自签名证书,请执行以下操作:

export IP=10.0.0.2
export CERTSTORE_SECRET=password    # (1)
keytool \
  -genkeypair \
  -alias rsakey \
  -keyalg rsa \
  -storepass ${CERTSTORE_SECRET} \
  -keystore certstore.jks \
  -storetype JKS \
  -dname "CN=${IP}.nip.io,O=Vert.x Development" # (2)
  1. 不要使用此密码!

  2. 将 CN 替换为您自己的 IP 地址(localhost 除外),后缀为 .nip.io

对于此设置,我们依赖于一个免费的 DNS 服务器,它在查询时返回您的 IP 地址。Web 上也存在其他提供相同结果的服务,例如:

目前我们有了一个 SSL 证书,但对于现代 Java 版本而言,其格式被认为是过时的,因此我们需要第二步将其转换为 PKCS#12

keytool \
  -importkeystore \
  -srckeystore certstore.jks \
  -destkeystore certstore.jks \
  -deststoretype pkcs12

您的新 SSL 证书位于 certstore.jks 文件中。

注册流程

在能够登录您的应用程序之前,我们需要注册一个 FIDO2 身份验证器。此过程类似于 Web 应用程序中的“注册”流程。然而,此图试图说明一些差异

register flow
  1. 用户仅使用用户名注册

  2. 您的服务器(依赖方)创建安全挑战

  3. 浏览器将此信息传递给令牌设备

  4. 令牌为此信息生成新的密钥对

  5. 挑战被签名并返回给服务器(RP)

  6. 服务器验证挑战是否正确并存储公钥

身份验证流程

authn flow
  1. 用户仅使用用户名进行身份验证

  2. 您的服务器(依赖方)创建安全挑战

  3. 浏览器将此信息传递给令牌设备

  4. 令牌生成验证凭据并签署挑战

  5. 浏览器创建身份验证断言

  6. 服务器验证断言是否正确并允许用户访问

编写服务器代码

处理身份验证器对象

您可以在此处找到完整的源代码,现在我们只介绍重要部分。

为了实现注册和身份验证功能,我们需要能够存储和查询身份验证器数据。为此,我们需要提供一些精确执行此操作的函数。您可以在此处查看其源代码。在您的 Verticle 中,您首先创建此对象,如下所示:

    // Dummy database, real world workloads
    // use a persistent store or course!
    InMemoryStore database = new InMemoryStore();

配置 WebAuthn 对象

为了使用 FIDO2,我们需要配置身份验证提供者如何工作。为此,我们需要创建并配置一个 WebAuthn 对象

    // create the webauthn security object
    WebAuthn webAuthN = WebAuthn.create(
      vertx,
      new WebAuthnOptions()   // (1)
        .setRelyingParty(new RelyingParty()
          .setName("Vert.x FIDO2/webauthn Howto"))
        .setUserVerification(UserVerification.DISCOURAGED)  // (2)
        .setAttestation(Attestation.NONE)   // (3)
        .setRequireResidentKey(false)   // (4)
        .setChallengeLength(64)   // (5)
        .addPubKeyCredParam(PublicKeyCredential.ES256)    // (6)
        .addPubKeyCredParam(PublicKeyCredential.RS256)
        .addTransport(AuthenticatorTransport.USB)   // (7)
        .addTransport(AuthenticatorTransport.NFC)
        .addTransport(AuthenticatorTransport.BLE)
        .addTransport(AuthenticatorTransport.INTERNAL))
      // where to load/update authenticators data
      .authenticatorFetcher(database::fetcher)
      .authenticatorUpdater(database::updater);
  1. 所有配置都在 WebAuthnOptions 中进行。这只是一个合理默认值的小例子,有关更多选项,请查阅 javadoc。

  2. 验证用户时,我们并不真正要求他们被验证。

  3. 在注册过程中,我们不希望验证硬件。

  4. 我们不需要常驻密钥来让用户进行身份验证。

  5. 定义挑战的长度,最小为 32

  6. 我们接受哪些安全算法。

  7. 我们允许从浏览器到身份验证器使用哪种传输方式。

Web 路由初始化

让我们开始配置我们的 HTTP 路由以确保安全。在使用 FIDO2 之前,有几个处理器始终需要到位

  • BodyHandler

  • SessionHandler

还建议使用 StaticHandler,因为整个过程既需要 vert.x 代码(我们目前正在探索的)和一个小的辅助 JavaScript 脚本。为了简化开发,vert.x 也通过 Maven 依赖提供了这种辅助功能

  <dependencies>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-web</artifactId>
    </dependency>
    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-auth-webauthn</artifactId>
    </dependency>
    <dependency>    <!--(1)-->
      <groupId>io.vertx</groupId>
      <artifactId>vertx-auth-webauthn</artifactId>
      <version>${vertx.version}</version>
      <classifier>client</classifier>
      <type>js</type>
    </dependency>
  </dependencies>
  1. 提供了一个简单的辅助脚本,用于与 vert.x 后端交互。

现在我们可以像这样初始化 Web 路由器

    final Router app = Router.router(vertx);
    app.route()   // (1)
      .handler(StaticHandler.create());
    app.post()    // (2)
      .handler(BodyHandler.create());
    app.route()   // (3)
      .handler(SessionHandler
        .create(LocalSessionStore.create(vertx)));

    WebAuthnHandler webAuthnHandler = WebAuthnHandler.create(webAuthN) // (4)
      .setOrigin(String.format("https://%s.nip.io:8443", System.getenv("IP")))
      // required callback
      .setupCallback(app.post("/webauthn/callback"))
      // optional register callback
      .setupCredentialsCreateCallback(app.post("/webauthn/register"))
      // optional login callback
      .setupCredentialsGetCallback(app.post("/webauthn/login"));

    app.route()
      .handler(webAuthnHandler);

    app.route("/protected")   // (5)
      .handler(ctx ->
        ctx.response()
          .end(
            "FIDO2 is Awesome!\n" +
              "No Password phishing here!\n"));
  1. 为客户端应用程序提供服务(稍后详述)。

  2. 启用 POST 请求体的解析。

  3. 启用会话。

  4. 使用之前定义的配置挂载 webauthn 处理器。

  5. 示例安全路由。

服务器引导

现在我们有了一个最小的路由器,我们需要创建一个 HTTPS 服务器。请注意,这是一个必需的步骤,也是我们为开发环境创建自签名证书并使用自定义域名k 的原因。

    vertx.createHttpServer(
      new HttpServerOptions()
        .setSsl(true)
        .setKeyStoreOptions(
          new JksOptions()
            .setPath("certstore.jks")
            .setPassword(System.getenv("CERTSTORE_SECRET"))))
      .requestHandler(app)
      .listen(8443, "0.0.0.0")
      .onSuccess(v -> {
        System.out.printf("Server: https://%s.nip.io:8443%n", System.getenv("IP"));
        start.complete();
      })
      .onFailure(start::fail);

至此,我们的后端应用程序已完成,路由 /protected 应受 FIDO2 保护。运行应用程序如下:

IP=10.0.0.2 \
mvn exec:java

# The following line should be present in your console:
# Server listening at: https://10.0.0.2.nip.io:8443

您的浏览器给出关于自签名证书的警告是正常的

selfsigned

这是为了您的保护。在真实的应用程序中,您应该使用适当的 SSL 证书,例如由 Let's Encrypt 颁发的证书。

用您的浏览器导航到:https://10.0.0.2.nip.io:8443/protected 应该会显示一个 Forbidden 错误。下一步是创建一个最小的登录和注册 Web 应用程序。

编写客户端代码

对于客户端应用程序,不使用任何框架,以表明该脚本可以与任何框架一起使用,也可以独立使用。

我们将创建一个包含 3 个部分的极简 HTML 页面

  1. 一个表单,用户只需输入以下内容即可注册或登录:

    • 显示名称:例如,一个用户友好的名称,如“John Doe”

    • 用户名:一个唯一的用户名,如“[email protected]

  2. 用于注册登录的按钮

  3. 一个指向受保护资源的链接,只有在注册登录后才允许访问。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8"/>
  <title>WebAuthn Howto</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

<div id="message"></div>
<div> <!--(1)-->
  <label for="displayName">Display Name: </label>
  <input id="displayName" name="displayName" type="text" value="Your Name"><br/>
  <label for="username">Username: </label>
  <input id="username" name="username" type="email" value="[email protected]">
</div>
<hr>
<div> <!--(2)-->
  <button id="register">register</button>
  <button id="login">login</button>
</div>
<hr>
<div> <!--(3)-->
  <a href="/protected">Want some secret info?</a>
</div>

<script src="vertx-auth-webauthn-client.js"></script>   <!--(4)-->
<script src="main.js"></script>   <!--(5)-->
</body>
</html>
  1. 如上所述的第一部分。

  2. 如上所述的第二部分。

  3. 如上所述的第三部分。

  4. 已添加到项目依赖项中的辅助脚本

  5. 您的应用程序脚本

客户端脚本代码

您可以在此处查看完整脚本。我们只介绍重要部分。

所包含的脚本 vertx-auth-webauthn-client.js 定义了一个全局类型 WebAuthn。第一步是创建此对象的一个实例并对其进行配置,使其与我们的后端配置匹配。

const webAuthn = new WebAuthn({
  callbackPath: '/webauthn/callback',
  registerPath: '/webauthn/register',
  loginPath: '/webauthn/login'
});

配置应该很简单,只需定义与您的后端路由匹配的路径即可。

接下来,我们需要为按钮添加事件处理程序。我们从注册操作开始

registerButton.onclick = () => {
  webAuthn
    .register({
      name: document.getElementById('username').value,
      displayName: document.getElementById('displayName').value
    })
    .then(() => {
      displayMessage('registration successful');
    })
    .catch(err => {
      displayMessage('registration failed');
      console.error(err);
    });
};

onclick 事件将只使用 Webauthn 对象。Webauthn 对象只有 2 个方法,它是基于 Promise 的,因此对于 vert.x 用户来说应该不难理解。如果您主要是 Java vert.x 开发人员,只需将 JavaScript Promise 视为 vert.x Future,那么一切都会非常相似。

要注册用户,只需要 2 个必需属性

  1. name 唯一的用户名,例如,电子邮件地址。

  2. displayName 人类友好的文本描述,例如您的名字和姓氏。

填写表单并点击注册按钮后,用户应该会看到一个弹出窗口,要求授权注册。

register start

触摸身份验证器后,流程应成功完成,显示

register end

此时您可以关闭浏览器甚至打开一个不同的浏览器。注册过程已完成,因此您现在可以使用您的令牌从任何地方登录。

我们现在需要处理登录按钮的 onclick 事件

loginButton.onclick = () => {
  webAuthn
    .login({
      name: document.getElementById('username').value
    })
    .then(() => {
      displayMessage('You are logged in');
    })
    .catch(err => {
      displayMessage('Invalid credential');
      console.error(err);
    });
};

正如 FIDO2 所描述的,使用 WebAuthn 是一种无密码身份验证,因此执行登录唯一需要的字段是

  1. name 唯一的用户名,例如,电子邮件地址。

login start

就像在注册屏幕中一样,将弹出一个窗口,告诉您有登录意图并请求用户授权。当您触摸身份验证器时,您可以看到

login end

现在您已登录,终于可以尝试查看秘密信息链接了,它应该会给您一些类似的东西

secret

总结

在本操作指南中,我们介绍了

  1. 创建一个 Web 项目

  2. 使用 Webauthn 保护 Web 应用程序

  3. 在 webauthn-client.js 的帮助下编写客户端代码

希望您现在可以在下一个项目中使用 FIDO2/webauthn 了!


最后发布:2025-06-11 02:03:27 +0000。