WebAuthn:如何识别错误指纹/PIN/图案验证尝试并限制验证次数
WebAuthn验证尝试次数限制与错误识别实现方案
一、错误尝试的识别方式
WebAuthn不会直接返回"指纹错误""PIN输错"这类具体失败原因,但可以通过两种核心方式判断验证失败:
- 客户端API异常捕获:调用
navigator.credentials.get()时,用户验证失败或主动取消操作,浏览器会抛出NotAllowedError或AbortError(不同浏览器存在差异,比如Chrome在验证失败时可能抛出AbortError,Firefox部分场景返回NotAllowedError)。 - 服务端断言验证结果:客户端将验证后的断言发送到服务端后,通过WebAuthn验证库(如
@simplewebauthn/server)校验时,若断言无效(比如签名不匹配),库会返回明确的错误信息,间接说明用户身份验证环节未通过。
二、尝试次数限制的实现
1. 客户端辅助限制(仅做体验优化)
可以在客户端用sessionStorage维护会话内的失败计数器,达到阈值后暂时禁用WebAuthn验证按钮。但这种方式不安全,用户可通过清空存储绕过,只能作为前端体验补充。
2. 服务端核心限制(必须实现)
这是最可靠的限制方式,需结合数据库存储用户验证状态:
- 数据库字段设计:为用户表添加
webauthn_failed_attempts(失败次数)和webauthn_lockout_until(锁定截止时间)两个字段。 - 核心流程逻辑:
- 用户发起验证请求时,先检查是否处于锁定状态(
webauthn_lockout_until晚于当前时间),若是直接返回锁定提示。 - 服务端验证断言失败时,递增
webauthn_failed_attempts计数。 - 当失败次数达到阈值(如5次),设置
webauthn_lockout_until为当前时间+锁定时长(如15分钟),同时重置失败计数。 - 验证成功时,将
webauthn_failed_attempts重置为0,清除锁定时间。
- 用户发起验证请求时,先检查是否处于锁定状态(
3. 异常捕获的细节处理
客户端捕获NotAllowedError/AbortError后,可向服务端发送"验证失败"请求触发计数递增,但要注意区分用户主动取消和真实验证失败:比如可以通过记录弹窗显示时长,若用户短时间内关闭弹窗,不算错误尝试,具体逻辑根据业务场景调整。
服务端验证断言失败时直接递增计数是最准确的方式,因为只有用户完成身份验证环节但未通过,才会生成无效断言。
三、服务端代码示例(Node.js + @simplewebauthn/server)
// 假设用户模型包含webauthnFailedAttempts、webauthnLockoutUntil等字段 async function verifyWebAuthnAssertion(userId, assertion) { const user = await User.findById(userId); // 检查是否处于锁定状态 if (user.webauthnLockoutUntil && new Date() < user.webauthnLockoutUntil) { throw new Error("WebAuthn验证已锁定,请15分钟后再试"); } try { const verificationResult = await verifyAuthenticationResponse({ response: assertion, expectedChallenge: user.latestWebAuthnChallenge, authenticator: user.webauthnAuthenticator, expectedOrigin: "https://your-domain.com", expectedRPID: "your-domain.com", }); // 验证成功,重置状态 user.webauthnFailedAttempts = 0; user.webauthnLockoutUntil = null; await user.save(); return verificationResult; } catch (error) { // 验证失败,更新计数与锁定状态 user.webauthnFailedAttempts += 1; if (user.webauthnFailedAttempts >= 5) { user.webauthnLockoutUntil = new Date(Date.now() + 15 * 60 * 1000); user.webauthnFailedAttempts = 0; } await user.save(); throw error; } }
内容的提问来源于stack exchange,提问作者canaryGrapher
相关产品推荐
相关产品推荐

