资讯动态

Epic Stack 无密码登录实践:基于 WebAuthn 与 SimpleWebAuthn 的 Passkey 完整接入方案

发布时间:2026/9/17 18:55:49 来源:尧图企业网站定制
Epic Stack 无密码登录实践基于 WebAuthn 与 SimpleWebAuthn 的 Passkey 完整接入方案【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack本文以 docs/decisions/039-passkeys.md 决策文档为骨架结合 Epic Stack 仓库中的路由实现、Prisma 数据模型与端到端测试系统讲解如何在 React Router 全栈应用中落地 WebAuthn 标准的 Passkey 认证包括注册与登录的完整握手流程、Passkey数据模型设计、多认证策略并存策略、安全管理细节与测试验证方案。为什么需要 Passkey传统认证方式的三大痛点在 Epic Stack 引入 Passkey 之前项目默认支持两种认证方式用户名/密码username/password与 OAuth 第三方登录。这两种方式覆盖了绝大多数应用场景但从安全与体验两个维度看都存在结构性短板。密码认证的固有问题详见 docs/decisions/039-passkeys.md 的 Context 章节用户习惯在多个服务间复用同一密码一旦某个站点数据泄露攻击者可撞库密码可被钓鱼phishing或窃取短信验证码同样可被拦截密码管理与定期更换对用户是持续负担密码找回reset流程本身是复杂的潜在攻击面OAuth 第三方登录的固有问题依赖第三方服务的可用性服务宕机时登录直接不可用存在向第三方共享用户数据的隐私顾虑并非所有用户都拥有或愿意使用社交账号第三方策略与回调配置带来额外运维成本WebAuthnWeb Authentication是 W3C 发布的网页认证标准用公钥密码学取代共享密钥密码。它允许网站注册两类认证器authenticator平台认证器platform authenticator设备内置的认证能力如 Touch ID、Face ID、Windows Hello以及 1Password 等密码管理器漫游认证器roaming authenticator独立于设备的硬件安全密钥security key或作为安全密钥使用的手机Passkey 的认证流程与安全收益决策文档给出了两条核心流程注册Registration流程服务端生成 challenge下发注册选项registration options客户端浏览器创建新的密钥对用私钥对 challenge 签名公钥与元数据发送回服务端存储私钥永远安全地保存在认证器内不离开设备认证Authentication流程服务端生成新的 challenge客户端用认证器中保存的私钥签名服务端用之前存储的公钥验签由此获得四个关键安全特性决策文档原文私钥永远不会离开认证器每个凭据credential与特定网站绑定天然防钓鱼生物识别 / PIN 验证在本地完成生物特征数据不经过网络服务端不存储任何共享密钥决策为什么选 SimpleWebAuthn 与多认证策略并存技术选型决策文档明确Passkey 支持基于simplewebauthn/server与simplewebauthn/browser实现。在当前仓库的 package.json 中可以看到依赖版本为simplewebauthn/browser: ^13.2.2与simplewebauthn/server: ^13.2.2。选择该库的理由维护活跃、被广泛使用客户端与服务端均为 TypeScript 类型安全实现封装了 WebAuthn 规范的大量复杂性支持所有主流浏览器与平台多认证策略并存而非激进替换决策文档强调Passkey 是未来但密码与 OAuth 必须继续保留理由有三过渡与采纳Adoption and TransitionPasskey 在各平台与浏览器仍在逐步铺开用户需要时间熟悉新交互企业内部可能对认证方式有既有要求兜底选项Fallback Options部分用户设备不兼容企业环境可能禁用生物识别多备份认证方式提升整体可靠性用户选择User Choice不同用户对安全/便捷的偏好不同特定场景需要特定认证类型多方式共存最大化可访问性这一结论与仓库现状一致登录页同时提供密码登录、GitHub OAuth 登录与 Passkey 登录三种入口认证体系整体说明可参考 docs/authentication.md。数据模型Passkey 的 Prisma 设计决策文档要求用一个专门的 Prisma 模型存储 Passkey跟踪三类信息认证器元数据AAGUID、设备类型、传输方式、安全信息公钥、计数器、用户关系与时间戳。仓库中的实现位于 prisma/schema.prismamodel Passkey { id String id aaguid String createdAt DateTime default(now()) updatedAt DateTime updatedAt publicKey Bytes user User relation(fields: [userId], references: [id], onDelete: Cascade) userId String webauthnUserId String counter BigInt deviceType String // singleDevice or multiDevice backedUp Boolean transports String? // Stored as comma-separated values index(userId) }各字段含义与决策文档一一对应字段类型说明idString id凭据 IDcredential ID全局唯一注册与登录时用它定位记录aaguidString认证器厂商与型号的唯一标识可用于帮助用户区分多个密码管理器publicKeyBytes用于验签的公钥COSE 格式字节counterBigInt认证器签名计数器用于防重放攻击登录成功后更新deviceTypeStringsingleDevice平台认证器或multiDevice跨平台安全密钥backedUpBoolean该凭据是否已被备份如经云同步transportsString?传输方式逗号分隔存储usb、nfc、ble、internal、hybrid、cable 等userId/webauthnUserIdString用户关系webauthnUserId是注册时下发的 User HandlecreatedAt/updatedAtDateTime创建与更新时间戳注意onDelete: Cascade与index(userId)用户删除时凭据级联删除且按用户查询走索引。transports以逗号分隔字符串存储是因为 SQLite 不原生支持数组属于该决策文档Neutral中性影响——新增数据存储与迁移部分的落地体现。源码实战一Passkey 注册全链路服务端注册路由注册路由位于 app/routes/_auth/webauthn/registration.ts拆成loader生成注册选项与action校验并落库两个端点均为 JSON APIloader —— 生成注册选项const options await generateRegistrationOptions({ rpName: config.rpName, rpID: config.rpID, userName: user.username, userID: new TextEncoder().encode(userId), userDisplayName: user.name ?? user.email, attestationType: none, excludeCredentials: passkeys, authenticatorSelection: { residentKey: preferred, userVerification: preferred, }, })关键参数解读rpID即依赖方 ID来自 app/routes/_auth/webauthn/utils.server.ts 的getWebAuthnConfig取当前请求域名的 hostname —— 这正是 Passkey 与域名绑定、防钓鱼的根本机制attestationType: none不要求认证器提供硬件级 attestation 证书兼顾隐私与兼容性excludeCredentials传入用户已注册的凭据 ID 列表避免同一认证器重复注册residentKey: preferred与userVerification: preferred倾向可发现凭据与用户验证但不强制兼容性更好生成的options.challenge通过passkeyCookie名为webauthn-challenge的 httpOnly Cookie随响应下发注册校验时再取回比对。Cookie 配置见 utils.server.tssameSite: lax、httpOnly: true、maxAge两小时、生产环境secure、用SESSION_SECRET签名。action —— 校验并落库const verification await verifyRegistrationResponse({ response: data, expectedChallenge: challenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: true, }) // ... 检查 credential 是否已注册 await prisma.passkey.create({ data: { id: credential.id, aaguid, publicKey: Buffer.from(credential.publicKey), userId, webauthnUserId, counter: credential.counter, deviceType: credentialDeviceType, backedUp: credentialBackedUp, transports: credential.transports?.join(,), }, })服务端校验四个要素expectedChallenge来自 Cookie、expectedOrigin当前站点 Origin、expectedRPIDhostname、requireUserVerification: true强制要求用户验证。校验通过后把公钥、计数器、AAGUID、设备类型、备份状态、传输方式写入数据库。若credential.id已存在则拒绝注册防止凭据重复。注意请求体与 Cookie 都经过 Zod SchemaRegistrationResponseSchema、PasskeyCookieSchema校验后再进入业务逻辑。客户端注册 UI设置页位于 app/routes/settings/profile/passkeys.tsx流程为const resp await fetch(/webauthn/registration) const { options } await resp.json() const regResult await startRegistration({ optionsJSON: options }) const verificationResp await fetch(/webauthn/registration, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(regResult), })startRegistration来自simplewebauthn/browser负责唤起浏览器的原生认证交互生物识别 / PIN / 安全密钥。注册成功后调用revalidator.revalidate()刷新列表。管理列表按注册时间倒序展示每条凭据显示类型标签deviceType platform显示 Device否则显示 Security Key相对注册时间formatDistanceToNow删除操作走actionintent delete时执行prisma.passkey.delete({ where: { id: passkeyId, userId } })——where同时限定userId从服务端保证只能删除自己的凭据。源码实战二Passkey 登录全链路服务端登录路由登录路由位于 app/routes/_auth/webauthn/authentication.tsloader —— 生成认证选项const options await generateAuthenticationOptions({ rpID: config.rpID, userVerification: preferred, }) // challenge 写入 webauthn-challenge Cookie登录阶段不需要userName因为 Passkey 登录是免输入用户名的discoverable credentials 场景挑战码随响应写入 Cookie。action —— 验签并建立会话const passkey await prisma.passkey.findUnique({ where: { id: authResponse.id }, include: { user: true }, }) const verification await verifyAuthenticationResponse({ response: authResponse, expectedChallenge: cookie.challenge, expectedOrigin: config.origin, expectedRPID: config.rpID, credential: { id: authResponse.id, publicKey: passkey.publicKey, counter: Number(passkey.counter), }, })验签通过后的三个关键步骤防重放prisma.passkey.update把counter更新为verification.authenticationInfo.newCounter计数器单调递增校验由 SimpleWebAuthn 内部完成决策文档将其列为counter字段的核心用途创建会话prisma.session.create写入新 session过期时间来自getSessionExpirationDate()app/utils/auth.server.ts接管登录调用handleNewSessionapp/routes/_auth/login.server.ts完成会话 Cookie 设置与重定向同时清除 challenge Cookie客户端登录入口登录页 app/routes/_auth/login.tsx 提供独立的 Login with a passkey 按钮状态机文案依次为Generating Authentication Options→Requesting your authorization→Verifying your passkey→Youre logged in! Navigating...const optionsResponse await fetch(/webauthn/authentication) const { options } await optionsResponse.json() const authResponse await startAuthentication({ optionsJSON: options }) const verificationResponse await fetch(/webauthn/authentication, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ authResponse, remember, redirectTo }), }) const { location } await verificationResponse.json() await navigate(location ?? /)整个登录过程与密码表单完全解耦用户无需输入用户名也无需输入任何密码体验与决策文档描述一致——用户尝试用 Passkey 登录时服务端生成挑战浏览器唤起已注册的 Passkey 认证成功后无需输入密码即完成登录。安全设计与输入校验Passkey 模块在 WebAuthn 协议安全之外还叠加了两层应用层防护Zod Schema 强校验utils.server.tsRegistrationResponseSchema与AuthenticationResponseSchema分别以satisfies z.ZodTypeRegistrationResponseJSON/satisfies z.ZodTypeAuthenticationResponseJSON约束精确校验字段类型、type: z.literal(public-key)、transports 枚举ble|cable|hybrid|internal|nfc|smart-card|usb等Challenge 状态隔离注册 challenge 与userId绑定存储于 CookiePasskeyCookieSchema登录 challenge 独立存储两者在 action 中取回比对未找到即拒绝No challenge foundCookie 安全属性httpOnly 防止 XSS 读取、sameSite: lax缓解 CSRF、生产环境强制secure、SESSION_SECRET签名防篡改测试验证CDP 虚拟认证器WebAuthn 依赖真实硬件交互无法在 CI 环境手动操作Epic Stack 的解法是通过 Chrome DevTools ProtocolCDP注入虚拟认证器见 tests/e2e/passkey.test.tsconst client await page.context().newCDPSession(page) await client.send(WebAuthn.enable, { enableUI: true }) await client.send(WebAuthn.addVirtualAuthenticator, { options: { protocol: ctap2, transport: usb, hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true, }, })虚拟认证器配置与决策文档的Mock authenticator support for development诉求一一对应ctap2协议、usb传输、支持常驻密钥与用户验证。测试断言的关键点初始状态凭据数量为 0点击 Register new passkey 后监听WebAuthn.credentialAdded事件等待注册完成注册后WebAuthn.getCredentials返回凭据数量为 1页面出现 passkeys 列表与 Registered ... ago 文案之后执行登出并以 Passkey 重新登录WebAuthn.credentialAsserted断言事件这套方案使完整注册→登录链路在 Playwright 中可自动化回归覆盖了决策文档 Consequences 中提到的New test infrastructure for WebAuthn / Mock authenticator support / Additional e2e test scenarios。落地代价与注意事项决策文档在 Consequences 部分坦诚列出了引入 Passkey 的代价结合源码可总结为三类正面收益Positive防钓鱼认证显著提升抗攻击能力公钥凭证与域名绑定硬件级安全生物识别 安全芯片强于纯密码用户可选密码、OAuth、Passkey 三种方式原生生物识别流程快速熟悉密码管理器集成带来跨设备无缝访问顺应 Web 标准与安全最佳实践演进为 Passkey 生态铺开做好准备负面代价NegativeWebAuthn 规范复杂需处理多样设备能力差异通过authenticatorSelection的preferred策略兼容必须长期维护密码认证作为兜底仓库中登录页三入口并存即为此新技术的用户教育成本需要清晰的文档与 UI 引导设置页的 Device / Security Key 类型标签即为此设计中性影响Neutral新增Passkey数据模型与每用户额外存储既有用户需主动前往设置页完成注册迁移路径新增 WebAuthn 测试设施CDP 虚拟认证器、mock 支持与 e2e 场景小结Epic Stack 的 Passkey 落地是一个完整的端到端工程实践决策层面论证了 WebAuthn 标准与多认证策略并存的必要性数据层面设计了跟踪认证器元数据、安全信息与用户关系的Passkey模型实现层面用simplewebauthn/server与simplewebauthn/browser支撑注册/登录两条 JSON 路由与设置页管理 UI测试层面用 CDP 虚拟认证器自动化了整个生命周期。如果你正在为自己的全栈应用接入无密码登录可以直接复用本仓库的 app/routes/_auth/webauthn 目录与 Passkey 模型同时保留密码与 OAuth 作为过渡期兜底。【免费下载链接】epic-stackThis is a Full Stack app starter with the foundational things setup and configured for you to hit the ground running on your next EPIC idea.项目地址: https://gitcode.com/GitHub_Trending/ep/epic-stack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价