Passkey认证实战:Next.js与NestJS无密码登录

为什么密码认证已经过时?
传统的密码登录模式,用户需要记住一串复杂字符,而开发者则需要处理哈希、加盐、限流和找回密码流程。即便如此,密码依然面临两大核心风险:
- 钓鱼攻击:一个仿冒页面就能骗走密码,用户很难辨别URL的细微差别。
- 密码复用:大多数人在多个网站重复使用相同密码,一旦某个数据库泄露,撞库攻击就会波及所有账户。
Passkey(通行密钥)正是为解决这些痛点而生。它基于WebAuthn标准,让用户通过设备内置的生物识别(指纹、面容)或PIN完成认证,私钥从不离开设备,服务器只保存公钥。这既消除了记忆密码的负担,也从根本上杜绝了钓鱼——因为伪造页面无法获得设备上的密钥签名。
技术选型与项目结构
本教程采用现代主流全栈方案:
- 后端:NestJS + PostgreSQL,负责用户与Passkey的存储、注册/认证挑战的生成与验证。
- 前端:Next.js,提供注册和登录界面,调用浏览器WebAuthn API。
- 核心库:SimpleWebAuthn,一个维护良好的TypeScript库,封装了WebAuthn的密码学操作与数据解析。
项目分为两个独立应用:backend(NestJS API)和frontend(Next.js),通过REST接口通信。
后端实现:注册与认证流程
1. 注册Passkey
当用户提供邮箱并请求注册时,后端先生成注册选项(包含挑战、依赖方ID等),前端调用navigator.credentials.create()让用户确认生物识别,返回认证器响应。后端再调用SimpleWebAuthn的verifyRegistrationResponse()验证签名,并将公钥和凭证ID存入数据库。
关键步骤:
- 生成挑战时,确保随机且每次唯一。
- 验证响应时,检查挑战是否匹配、来源是否可信。
- 凭证ID需要Base64URL编码存储,避免二进制乱码。
2. 认证(登录)Passkey
登录流程类似:后端生成认证选项,前端调用navigator.credentials.get()获取签名,后端用存储的公钥验证签名。验证通过后签发JWT,前端将其保存在HttpOnly Cookie或内存中,用于后续请求鉴权。
前端交互:从注册到登录
Next.js前端只需两个页面:
- 注册页:用户输入邮箱,点击“注册”后调用后端API获取注册选项,再触发浏览器原生认证弹窗。成功后显示“注册完成”。
- 登录页:输入邮箱,点击“登录”,同样触发认证弹窗,成功后跳转至受保护页面。
为了提升体验,务必捕获并展示错误信息,例如用户取消认证、设备不支持WebAuthn等。
安全避坑指南
教程作者特别提醒了四个容易踩的坑:
- 重复编码凭证ID:在存储和传输时只编码一次,否则验证时会因格式不匹配而失败。
- 用
undefined清除挑战:认证完成后,应将会话中的挑战置为undefined而非null,避免类型错误。 - 不检查凭证归属:验证认证响应时,必须确认该凭证确实属于当前用户,防止他人凭证被冒用。
- 注册时合并已有账户:若邮箱已存在,应明确提示或走绑定流程,而不是默默添加新Passkey,避免账户劫持。
另外,注册时要求用户验证(如设备PIN)是必要的,不要为了流畅而省略。
总结与下一步
通过本教程,你已经掌握了Passkey认证的完整闭环:后端生成挑战、前端调用WebAuthn、后端验证签名、签发JWT。这套方案不仅提升了安全性,也大幅改善了用户体验。
想进一步优化,可以考虑:
- 增加多设备Passkey同步策略。
- 使用
@simplewebauthn/server的MetadataService验证认证器是否符合FIDO规范。 - 为旧用户保留密码登录作为降级方案,逐步迁移。
完整源码已发布在GitHub,建议结合官方文档动手实践,才能真正理解WebAuthn的细节。






本文目录(8 节)
本文基于 freeCodeCamp 的公开内容,由 AI 辅助整理改写后发布。
原标题:How to Build Passkey Authentication with WebAuthn, Next.js and NestJS
阅读原文