You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React+ASP.NET Core无用户名登录:如何用JS检测已注册生物识别凭证?

无用户名登录中检测已注册生物识别凭证的实现方案

核心结论

可以通过JavaScript间接检测已注册的WebAuthn生物识别凭证,但无法直接枚举或获取凭证的具体信息——只能通过发起WebAuthn断言请求,根据请求结果判断是否存在匹配当前应用的已注册凭证。

实现思路(结合你的技术栈)

基于React+webauthn-json前端、ASP.NET Core+fido2-net-lib后端的流程如下:

  • 前端点击登录按钮时,先向后端请求无用户名的断言挑战选项
  • 前端用webauthn-json发起断言请求,若浏览器找到匹配的凭证则返回结果,否则抛出特定错误
  • 根据请求结果,要么继续完成登录验证,要么提示用户无可用凭证

代码示例

前端(React + webauthn-json)

import { get } from "@github/webauthn-json";

const BiometricLoginButton = () => {
  const handleLogin = async () => {
    try {
      // 1. 获取后端生成的断言选项
      const optionsRes = await fetch("/api/auth/start-username-less-login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
      });
      const assertionOptions = await optionsRes.json();

      // 2. 发起WebAuthn断言请求
      const assertion = await get(assertionOptions);

      // 3. 提交断言结果给后端验证
      const verifyRes = await fetch("/api/auth/verify-username-less-login", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(assertion),
      });

      if (verifyRes.ok) {
        // 登录成功,跳转或更新应用状态
        window.location.href = "/dashboard";
      } else {
        alert("登录验证失败,请重试");
      }
    } catch (err) {
      // 捕获无凭证或用户取消的错误
      if (err.name === "NoCredentialsError" || err.name === "NotAllowedError") {
        alert("未检测到可用的生物识别凭证,请先完成注册");
      } else {
        console.error("登录出错:", err);
        alert("登录过程中出现未知错误");
      }
    }
  };

  return <button onClick={handleLogin}>使用生物识别登录</button>;
};

export default BiometricLoginButton;

后端(ASP.NET Core + fido2-net-lib)

using Fido2NetLib;
using Microsoft.AspNetCore.Http;
using System.Text.Json;

[ApiController]
[Route("api/auth")]
public class AuthController : ControllerBase
{
    private readonly IFido2 _fido2;
    private readonly ICredentialRepository _credentialRepo; // 自定义的凭证仓储

    public AuthController(IFido2 fido2, ICredentialRepository credentialRepo)
    {
        _fido2 = fido2;
        _credentialRepo = credentialRepo;
    }

    [HttpPost("start-username-less-login")]
    public IActionResult StartUsernameLessLogin()
    {
        // 生成允许"发现用户"的断言选项:空的AllowCredentials表示匹配所有当前RP的凭证
        var assertionOptions = _fido2.GetAssertionOptions(
            rpId: "your-app-domain.com", // 替换为你的应用RP ID(需与域名匹配)
            userVerification: UserVerificationRequirement.Preferred,
            allowCredentials: new List<PublicKeyCredentialDescriptor>()
        );

        // 将选项存入会话,用于后续验证
        HttpContext.Session.SetString("AssertionOptions", JsonSerializer.Serialize(assertionOptions));

        return Ok(assertionOptions);
    }

    [HttpPost("verify-username-less-login")]
    public async Task<IActionResult> VerifyUsernameLessLogin([FromBody] AuthenticatorAssertionRawResponse assertion)
    {
        var storedOptions = JsonSerializer.Deserialize<AssertionOptions>(
            HttpContext.Session.GetString("AssertionOptions")
        );
        if (storedOptions == null) return BadRequest("无效的请求状态");

        // 根据凭证ID从数据库查找对应的已注册凭证
        var storedCredential = await _credentialRepo.GetByCredentialId(assertion.Id);
        if (storedCredential == null) return BadRequest("未找到匹配的凭证");

        // 验证断言有效性
        var verifyResult = _fido2.VerifyAssertion(
            storedOptions,
            assertion,
            storedCredential.PublicKey,
            storedCredential.SignCount,
            IPAddress.Parse(HttpContext.Connection.RemoteIpAddress.ToString())
        );

        if (verifyResult.IsSuccess)
        {
            // 更新凭证的签名计数,防止重放攻击
            storedCredential.SignCount = verifyResult.Counter;
            await _credentialRepo.Update(storedCredential);

            // 生成JWT或其他登录凭证返回给前端
            var jwtToken = GenerateJwt(storedCredential.UserId);
            return Ok(new { Token = jwtToken });
        }

        return BadRequest("登录验证失败");
    }

    // 自定义JWT生成方法,根据你的需求实现
    private string GenerateJwt(Guid userId)
    {
        // 省略JWT生成逻辑
        return "your-jwt-token";
    }
}

关键注意事项

  • 浏览器限制:无法直接枚举用户设备上的所有WebAuthn凭证,只能通过断言请求间接检测,且仅能获取与当前应用RP ID匹配的凭证。
  • 错误区分:NoCredentialsError表示确实无匹配凭证,NotAllowedError通常是用户主动取消了验证,可根据场景调整提示文案。
  • RP ID匹配:RP ID必须与当前页面域名一致或为其上级域名(例如页面域名是app.yourdomain.com,RP ID可以是yourdomain.com或app.yourdomain.com),否则浏览器不会返回任何凭证。

内容的提问来源于stack exchange,提问作者Léster

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 01:40:12