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
相关产品推荐
相关产品推荐

