Java与WebAuthn(FIDO2):实现无密码、强认证的身份验证机制

Java与WebAuthn(FIDO2):实现无密码、强认证的身份验证机制

大家好,今天我们来深入探讨如何利用Java和WebAuthn(FIDO2)构建一套无密码、强认证的身份验证机制。WebAuthn的出现,为我们提供了一种安全、便捷、且标准化的方式,取代传统的密码验证,显著提升用户账户的安全性。

1. WebAuthn 概述

WebAuthn(Web Authentication)是由 W3C 和 FIDO 联盟共同制定的开放标准。它允许网站和应用程序利用各种认证器(Authenticator)进行用户身份验证,而无需依赖传统的密码。这些认证器可以是硬件安全密钥(如 YubiKey)、指纹扫描仪、面部识别器,甚至是智能手机上的平台认证器。

1.1 WebAuthn 的核心概念:

  • 依赖方 (Relying Party, RP): 通常是网站或应用程序,负责发起和验证 WebAuthn 认证过程。 在我们的示例中,这将是我们的 Java 后端服务器和前端 Web 应用程序。
  • 用户代理 (User Agent): 用户的浏览器,负责与认证器进行通信,并将结果传递给依赖方。
  • 认证器 (Authenticator): 提供认证功能的硬件或软件设备。它生成和存储密钥对,并执行加密操作以验证用户身份。
  • 凭证 (Credential): 由认证器创建并存储的密钥对,用于在后续的认证过程中验证用户身份。凭证与特定的依赖方和用户账户相关联。

1.2 WebAuthn 的优势:

  • 安全性: 使用非对称密钥加密,密钥存储在认证器硬件中,防止密码泄露和重放攻击。
  • 便捷性: 用户可以使用生物识别或硬件密钥进行身份验证,无需记住复杂的密码。
  • 标准化: WebAuthn 是一种开放标准,得到主流浏览器和操作系统的支持,易于集成和部署。
  • 防钓鱼: 凭证与特定的域名绑定,有效防止钓鱼攻击。

2. WebAuthn 的工作流程

WebAuthn 的工作流程主要包括两个阶段:注册 (Registration) 和认证 (Authentication)。

2.1 注册 (Registration):

  1. 依赖方 (RP) 发起注册: RP 向用户代理发送注册请求,包含 RP ID (通常是域名)、用户 ID 和其他可选参数。
  2. 用户代理与认证器交互: 用户代理接收到注册请求后,提示用户选择认证器并进行验证(例如,插入安全密钥、扫描指纹)。
  3. 认证器生成凭证: 认证器生成一个新的密钥对,并将公钥部分返回给用户代理。私钥部分安全地存储在认证器中。
  4. 用户代理将凭证返回给 RP: 用户代理将公钥和凭证元数据(例如,认证器类型、凭证 ID)发送回 RP。
  5. RP 存储凭证: RP 将公钥和凭证元数据与用户账户关联存储在数据库中。

2.2 认证 (Authentication):

  1. 依赖方 (RP) 发起认证: RP 向用户代理发送认证请求,包含 RP ID 和允许使用的凭证 ID 列表。
  2. 用户代理与认证器交互: 用户代理接收到认证请求后,提示用户选择认证器并进行验证。
  3. 认证器生成签名: 认证器使用存储的私钥对挑战 (Challenge) 进行签名。
  4. 用户代理将签名返回给 RP: 用户代理将签名和凭证 ID 发送回 RP。
  5. RP 验证签名: RP 使用存储的公钥验证签名是否有效。如果验证成功,则用户身份验证通过。

3. Java 后端实现 WebAuthn

接下来,我们将使用 Java 实现 WebAuthn 的后端逻辑。我们将使用 java-webauthn-server 库,它简化了 WebAuthn 的集成过程。

3.1 添加依赖:

首先,需要在 pom.xml 文件中添加 java-webauthn-server 库的依赖:

<dependency>
    <groupId>com.github.webauthn-json</groupId>
    <artifactId>webauthn-server-json</artifactId>
    <version>0.1.0</version>
</dependency>

3.2 配置 WebAuthn 服务:

import com.webauthn4j.server.WebAuthnServer;
import com.webauthn4j.server.WebAuthnServerCore;
import com.webauthn4j.server.ServerProperty;
import com.webauthn4j.server.ServerPropertyProvider;
import com.webauthn4j.util.Base64UrlUtil;

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

public class WebAuthnService {

    private final WebAuthnServer webAuthnServer;
    private final String relyingPartyId;
    private final String relyingPartyName;

    public WebAuthnService(String relyingPartyId, String relyingPartyName) {
        this.relyingPartyId = relyingPartyId;
        this.relyingPartyName = relyingPartyName;

        ServerPropertyProvider serverPropertyProvider = (clientDataHash) -> {
            Set<String> origins = Collections.singleton(relyingPartyId);
            return new ServerProperty(relyingPartyId, relyingPartyName, clientDataHash, origins, null);
        };

        this.webAuthnServer = new WebAuthnServerCore(serverPropertyProvider);
    }

    public WebAuthnServer getWebAuthnServer() {
        return webAuthnServer;
    }

    public String getRelyingPartyId() {
        return relyingPartyId;
    }

    public String getRelyingPartyName() {
        return relyingPartyName;
    }
}

解释:

  • WebAuthnService 类封装了 WebAuthn Server 的配置。
  • relyingPartyId 通常是你的域名。
  • relyingPartyName 是你的应用程序名称。
  • ServerPropertyProvider 提供 ServerProperty 对象,其中包含 RP ID、RP Name 和 Client Data Hash 等信息。 Client Data Hash 是从前端获取的,用于防止重放攻击。

3.3 注册流程实现:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.webauthn4j.data.PublicKeyCredentialCreationOptions;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.data.PublicKeyCredentialParameters;
import com.webauthn4j.data.PublicKeyCredentialType;
import com.webauthn4j.data.PublicKeyCredentialDescriptor;
import com.webauthn4j.data.AuthenticatorSelectionCriteria;
import com.webauthn4j.data.AuthenticatorAttachment;
import com.webauthn4j.data.ResidentKeyRequirement;
import com.webauthn4j.data.AttestationConveyancePreference;
import com.webauthn4j.data.UserVerificationRequirement;
import com.webauthn4j.data.AuthenticatorTransport;
import com.webauthn4j.data.RegistrationData;
import com.webauthn4j.validator.RegistrationContextValidationResponse;
import com.webauthn4j.data.attestation.AttestationObject;
import com.webauthn4j.data.client.ClientRegistrationResponse;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.UUID;

public class RegistrationController {

    private final WebAuthnService webAuthnService;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final UserRepository userRepository;

    public RegistrationController(WebAuthnService webAuthnService, UserRepository userRepository) {
        this.webAuthnService = webAuthnService;
        this.userRepository = userRepository;
    }

    public void startRegistration(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        User user = userRepository.findByUsername(username);
        if (user == null) {
            user = new User();
            user.setUsername(username);
            user.setId(UUID.randomUUID().toString()); // Assign a unique ID
            userRepository.save(user);
        }

        Challenge challenge = new DefaultChallenge();
        request.getSession().setAttribute("registrationChallenge", challenge);
        request.getSession().setAttribute("registrationUser", user);

        PublicKeyCredentialCreationOptions registrationOptions = createRegistrationOptions(user.getId(), user.getUsername(), challenge);

        String json = objectMapper.writeValueAsString(registrationOptions);
        response.setContentType("application/json");
        response.getWriter().write(json);
    }

    private PublicKeyCredentialCreationOptions createRegistrationOptions(String userId, String username, Challenge challenge) {
        // See: https://www.w3.org/TR/webauthn-2/#dictionary-credential-params
        List<PublicKeyCredentialParameters> pubKeyCredParams = new ArrayList<>();
        pubKeyCredParams.add(new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -7)); // ES256
        pubKeyCredParams.add(new PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, -257)); // RS256

        // See: https://www.w3.org/TR/webauthn-2/#dictdef-publickeycredentialuserentity
        String userDisplayName = username;
        byte[] userIdBytes = userId.getBytes();

        // See: https://www.w3.org/TR/webauthn-2/#dictdef-authenticatorselectioncriteria
        AuthenticatorSelectionCriteria authenticatorSelectionCriteria = new AuthenticatorSelectionCriteria(
                AuthenticatorAttachment.PLATFORM,
                true,
                UserVerificationRequirement.PREFERRED,
                ResidentKeyRequirement.REQUIRED
        );

        // See: https://www.w3.org/TR/webauthn-2/#enumdef-attestationconveyancepreference
        AttestationConveyancePreference attestation = AttestationConveyancePreference.NONE;

        return new PublicKeyCredentialCreationOptions(
                webAuthnService.getRelyingPartyId(),
                userIdBytes,
                username,
                userDisplayName,
                challenge,
                pubKeyCredParams,
                60000L, // Timeout in milliseconds
                Collections.emptyList(), // excludeCredentials
                authenticatorSelectionCriteria,
                attestation,
                null
        );
    }

    public void finishRegistration(HttpServletRequest request, HttpServletResponse response) throws IOException {
        User user = (User) request.getSession().getAttribute("registrationUser");
        Challenge challenge = (Challenge) request.getSession().getAttribute("registrationChallenge");

        if (user == null || challenge == null) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().write("Invalid registration state");
            return;
        }

        try {
            JsonNode registrationResponseJson = objectMapper.readTree(request.getReader());

            // Extract data from the registration response
            String clientDataJSONString = registrationResponseJson.get("response").get("clientDataJSON").asText();
            String attestationObjectString = registrationResponseJson.get("response").get("attestationObject").asText();
            String credentialIdString = registrationResponseJson.get("id").asText();
            String credentialTypeString = registrationResponseJson.get("type").asText();

            // Decode Base64URL encoded strings
            byte[] clientDataJSON = Base64UrlUtil.decode(clientDataJSONString);
            byte[] attestationObjectBytes = Base64UrlUtil.decode(attestationObjectString);
            byte[] credentialId = Base64UrlUtil.decode(credentialIdString);

            // Construct the AttestationObject
            AttestationObject attestationObject = AttestationObject.fromBytes(attestationObjectBytes);

            // Construct the ClientRegistrationResponse
            ClientRegistrationResponse clientRegistrationResponse = new ClientRegistrationResponse(clientDataJSON, attestationObjectString);

            // Build RegistrationData object
            RegistrationData registrationData = new RegistrationData(clientRegistrationResponse, attestationObject);

            RegistrationContextValidationResponse registrationContextValidationResponse = webAuthnService.getWebAuthnServer().validate(
                    registrationData,
                    challenge.getValue(),
                    webAuthnService.getRelyingPartyId(),
                    Collections.singleton(webAuthnService.getRelyingPartyId())
            );

            // Retrieve validated key information
            byte[] publicKey = registrationContextValidationResponse.getAttestationObject().getAuthenticatorData().getAttestedCredentialData().getCredentialPublicKey();

            // Store credential information in the database
            Credential credential = new Credential();
            credential.setUserId(user.getId());
            credential.setCredentialId(credentialId);
            credential.setPublicKey(publicKey);
            credential.setAttestationType(registrationContextValidationResponse.getAttestationObject().getFormat());
            userRepository.saveCredential(credential);

            request.getSession().removeAttribute("registrationChallenge");
            request.getSession().removeAttribute("registrationUser");

            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().write("Registration successful");

        } catch (Exception e) {
            e.printStackTrace();
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().write("Registration failed: " + e.getMessage());
        }
    }
}

解释:

  • startRegistration 方法:
    • 生成一个随机的 Challenge,并将其存储在 Session 中。
    • 创建 PublicKeyCredentialCreationOptions 对象,其中包含注册所需的参数。
    • PublicKeyCredentialCreationOptions 对象序列化为 JSON,并返回给前端。
  • createRegistrationOptions 方法:
    • 创建 PublicKeyCredentialCreationOptions 对象,其中包含 RP ID、用户 ID、用户名、Challenge、支持的公钥算法等信息。
  • finishRegistration 方法:
    • 从 Session 中获取 ChallengeUser 对象。
    • 从请求中获取注册响应数据。
    • 使用 webAuthnService.getWebAuthnServer().validate() 方法验证注册响应。
    • 如果验证成功,则将凭证信息存储到数据库中。

3.4 认证流程实现:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.webauthn4j.data.PublicKeyCredentialRequestOptions;
import com.webauthn4j.data.client.challenge.Challenge;
import com.webauthn4j.data.client.challenge.DefaultChallenge;
import com.webauthn4j.data.PublicKeyCredentialDescriptor;
import com.webauthn4j.data.AuthenticatorTransport;
import com.webauthn4j.data.AuthenticationData;
import com.webauthn4j.validator.AuthenticationContextValidationResponse;
import com.webauthn4j.data.client.ClientAuthenticationResponse;
import com.webauthn4j.util.Base64UrlUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.stream.Collectors;

public class AuthenticationController {

    private final WebAuthnService webAuthnService;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final UserRepository userRepository;

    public AuthenticationController(WebAuthnService webAuthnService, UserRepository userRepository) {
        this.webAuthnService = webAuthnService;
        this.userRepository = userRepository;
    }

    public void startAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        String username = request.getParameter("username");
        User user = userRepository.findByUsername(username);
        if (user == null) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            response.getWriter().write("User not found");
            return;
        }

        Challenge challenge = new DefaultChallenge();
        request.getSession().setAttribute("authenticationChallenge", challenge);
        request.getSession().setAttribute("authenticationUser", user);

        List<Credential> credentials = userRepository.findCredentialsByUserId(user.getId());
        PublicKeyCredentialRequestOptions authenticationOptions = createAuthenticationOptions(credentials, challenge);

        String json = objectMapper.writeValueAsString(authenticationOptions);
        response.setContentType("application/json");
        response.getWriter().write(json);
    }

    private PublicKeyCredentialRequestOptions createAuthenticationOptions(List<Credential> credentials, Challenge challenge) {
        List<PublicKeyCredentialDescriptor> allowCredentials = credentials.stream()
                .map(credential -> new PublicKeyCredentialDescriptor(
                        com.webauthn4j.data.PublicKeyCredentialType.PUBLIC_KEY,
                        credential.getCredentialId(),
                        Collections.emptyList()
                ))
                .collect(Collectors.toList());

        return new PublicKeyCredentialRequestOptions(
                challenge,
                60000L, // Timeout in milliseconds
                webAuthnService.getRelyingPartyId(),
                allowCredentials,
                UserVerificationRequirement.PREFERRED
        );
    }

    public void finishAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException {
        User user = (User) request.getSession().getAttribute("authenticationUser");
        Challenge challenge = (Challenge) request.getSession().getAttribute("authenticationChallenge");

        if (user == null || challenge == null) {
            response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            response.getWriter().write("Invalid authentication state");
            return;
        }

        try {
            JsonNode authenticationResponseJson = objectMapper.readTree(request.getReader());

            // Extract data from the authentication response
            String credentialIdString = authenticationResponseJson.get("id").asText();
            String clientDataJSONString = authenticationResponseJson.get("response").get("clientDataJSON").asText();
            String authenticatorDataString = authenticationResponseJson.get("response").get("authenticatorData").asText();
            String signatureString = authenticationResponseJson.get("response").get("signature").asText();
            String userHandleString = authenticationResponseJson.get("response").has("userHandle") ? authenticationResponseJson.get("response").get("userHandle").asText() : null;

            // Decode Base64URL encoded strings
            byte[] credentialId = Base64UrlUtil.decode(credentialIdString);
            byte[] clientDataJSON = Base64UrlUtil.decode(clientDataJSONString);
            byte[] authenticatorDataBytes = Base64UrlUtil.decode(authenticatorDataString);
            byte[] signature = Base64UrlUtil.decode(signatureString);
            byte[] userHandle = (userHandleString != null) ? Base64UrlUtil.decode(userHandleString) : null;

            // Construct the ClientAuthenticationResponse
            ClientAuthenticationResponse clientAuthenticationResponse = new ClientAuthenticationResponse(clientDataJSON, authenticatorDataString, signature, userHandleString);

            // Build AuthenticationData object
            AuthenticationData authenticationData = new AuthenticationData(clientAuthenticationResponse, credentialId);

            Credential credential = userRepository.findCredentialByCredentialId(credentialId);
            if (credential == null) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("Credential not found");
                return;
            }

            AuthenticationContextValidationResponse authenticationContextValidationResponse = webAuthnService.getWebAuthnServer().validate(
                    authenticationData,
                    challenge.getValue(),
                    webAuthnService.getRelyingPartyId(),
                    Collections.singleton(webAuthnService.getRelyingPartyId()),
                    credential.getPublicKey()
            );

            // Perform additional checks, such as user verification
            if (!authenticationContextValidationResponse.getAuthenticatorData().isUserPresent()) {
                response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                response.getWriter().write("User not present");
                return;
            }

            // Authentication successful
            request.getSession().removeAttribute("authenticationChallenge");
            request.getSession().removeAttribute("authenticationUser");

            response.setStatus(HttpServletResponse.SC_OK);
            response.getWriter().write("Authentication successful");

        } catch (Exception e) {
            e.printStackTrace();
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            response.getWriter().write("Authentication failed: " + e.getMessage());
        }
    }
}

解释:

  • startAuthentication 方法:
    • 生成一个随机的 Challenge,并将其存储在 Session 中。
    • 从数据库中检索用户的凭证列表。
    • 创建 PublicKeyCredentialRequestOptions 对象,其中包含认证所需的参数。
    • PublicKeyCredentialRequestOptions 对象序列化为 JSON,并返回给前端。
  • createAuthenticationOptions 方法:
    • 创建 PublicKeyCredentialRequestOptions 对象,其中包含 Challenge、允许使用的凭证 ID 列表等信息。
  • finishAuthentication 方法:
    • 从 Session 中获取 ChallengeUser 对象。
    • 从请求中获取认证响应数据。
    • 使用 webAuthnService.getWebAuthnServer().validate() 方法验证认证响应。
    • 如果验证成功,则用户身份验证通过。

3.5 User 和 Credential 的数据模型:

public class User {
    private String id;
    private String username;

    // Getters and setters
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

public class Credential {
    private String userId;
    private byte[] credentialId;
    private byte[] publicKey;
    private String attestationType;

    // Getters and setters
    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public byte[] getCredentialId() {
        return credentialId;
    }

    public void setCredentialId(byte[] credentialId) {
        this.credentialId = credentialId;
    }

    public byte[] getPublicKey() {
        return publicKey;
    }

    public void setPublicKey(byte[] publicKey) {
        this.publicKey = publicKey;
    }

        public String getAttestationType() {
        return attestationType;
    }

    public void setAttestationType(String attestationType) {
        this.attestationType = attestationType;
    }
}

//UserRepository interface (Example, you can use any ORM or database access method)
interface UserRepository {
    User findByUsername(String username);
    void save(User user);
    List<Credential> findCredentialsByUserId(String userId);
    Credential findCredentialByCredentialId(byte[] credentialId);
    void saveCredential(Credential credential);
}

3.6 数据库存储:

你需要选择一个数据库来存储用户信息和凭证信息。 以下是一个简单的表格,展示了可能的数据库结构:

表名 列名 数据类型 说明
users id VARCHAR(36) 用户 ID (UUID)
username VARCHAR(255) 用户名
credentials user_id VARCHAR(36) 用户 ID (外键,关联 users 表)
credential_id BLOB 凭证 ID (Base64URL 编码)
public_key BLOB 公钥 (DER 编码)
attestation_type VARCHAR(255) 认证声明类型 (例如: fido-u2f, packed)

4. 前端实现 WebAuthn

前端需要使用 WebAuthn API 与浏览器和认证器进行交互。 以下是使用 JavaScript 的示例代码:

4.1 注册流程:

async function register() {
    const username = document.getElementById("username").value;

    // 1. Start Registration
    const registrationOptions = await fetch("/registration/start?username=" + username)
        .then(response => response.json());

    // Convert base64url to ArrayBuffer
    registrationOptions.challenge = base64urlToArrayBuffer(registrationOptions.challenge);
    registrationOptions.user.id = base64urlToArrayBuffer(registrationOptions.user.id);
    if (registrationOptions.excludeCredentials) {
        registrationOptions.excludeCredentials = registrationOptions.excludeCredentials.map(c => {
            c.id = base64urlToArrayBuffer(c.id);
            return c;
        });
    }

    // 2. Call navigator.credentials.create()
    const credential = await navigator.credentials.create({
        publicKey: registrationOptions
    });

    // Convert ArrayBuffer to base64url
    const attestationObject = arrayBufferToBase64url(credential.response.attestationObject);
    const clientDataJSON = arrayBufferToBase64url(credential.response.clientDataJSON);
    const rawId = arrayBufferToBase64url(credential.rawId);

    // 3. Send Registration Response to Server
    const registrationResult = await fetch("/registration/finish", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({
            id: rawId,
            type: credential.type,
            response: {
                attestationObject: attestationObject,
                clientDataJSON: clientDataJSON
            }
        })
    }).then(response => response.text());

    alert(registrationResult);
}

// Utility functions for base64url encoding/decoding
function base64urlToArrayBuffer(base64url) {
    const padding = '='.repeat((4 - base64url.length % 4) % 4);
    const base64 = (base64url + padding)
        .replace(/-/g, '+')
        .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

function arrayBufferToBase64url(buffer) {
    let binary = '';
    let bytes = new Uint8Array(buffer);
    let len = bytes.byteLength;
    for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return btoa(binary)
        .replace(/+/g, '-')
        .replace(///g, '_')
        .replace(/=+$/, '');
}

4.2 认证流程:

async function authenticate() {
    const username = document.getElementById("username").value;

    // 1. Start Authentication
    const authenticationOptions = await fetch("/authentication/start?username=" + username)
        .then(response => response.json());

    // Convert base64url to ArrayBuffer
    authenticationOptions.challenge = base64urlToArrayBuffer(authenticationOptions.challenge);
    if (authenticationOptions.allowCredentials) {
        authenticationOptions.allowCredentials = authenticationOptions.allowCredentials.map(c => {
            c.id = base64urlToArrayBuffer(c.id);
            return c;
        });
    }

    // 2. Call navigator.credentials.get()
    const credential = await navigator.credentials.get({
        publicKey: authenticationOptions
    });

    // Convert ArrayBuffer to base64url
    const clientDataJSON = arrayBufferToBase64url(credential.response.clientDataJSON);
    const authenticatorData = arrayBufferToBase64url(credential.response.authenticatorData);
    const signature = arrayBufferToBase64url(credential.response.signature);
    const userHandle = credential.response.userHandle ? arrayBufferToBase64url(credential.response.userHandle) : null;
    const rawId = arrayBufferToBase64url(credential.rawId);

    // 3. Send Authentication Response to Server
    const authenticationResult = await fetch("/authentication/finish", {
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify({
            id: rawId,
            type: credential.type,
            response: {
                clientDataJSON: clientDataJSON,
                authenticatorData: authenticatorData,
                signature: signature,
                userHandle: userHandle
            }
        })
    }).then(response => response.text());

    alert(authenticationResult);
}

解释:

  • 前端代码使用 navigator.credentials.create()navigator.credentials.get() 方法与浏览器和认证器进行交互。
  • 需要将 Base64URL 编码的字符串转换为 ArrayBuffer,然后再传递给 WebAuthn API。
  • 需要将 ArrayBuffer 转换为 Base64URL 编码的字符串,然后再发送给后端服务器。

5. 安全性考虑

  • Challenge 的随机性: 确保 Challenge 是高度随机且不可预测的,以防止重放攻击。
  • Client Data Hash 验证: 在后端验证 Client Data Hash,以确保客户端发送的数据没有被篡改。
  • Attestation 验证: 验证 Attestation 对象,以确保认证器是可信的。
  • 用户验证: 强制用户验证(例如,使用指纹或 PIN 码)以增加安全性。
  • 传输层安全: 使用 HTTPS 协议来保护通信安全。

6. 注意事项

  • 浏览器支持: 确保你的目标浏览器支持 WebAuthn API。
  • 认证器兼容性: 不同的认证器可能具有不同的特性和功能。
  • 错误处理: 在前端和后端实现完善的错误处理机制。
  • 用户体验: 设计友好的用户界面,引导用户完成注册和认证过程。
  • 密钥管理: 安全地存储和管理用户的公钥和凭证信息。

7. 总结

通过结合 Java 后端和 JavaScript 前端,我们成功地构建了一个基于 WebAuthn 的无密码身份验证系统。 这不仅提升了安全性,也改善了用户体验。希望今天的分享能帮助大家更好地理解和应用 WebAuthn 技术。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注