# demo-passkey **Repository Path**: coco-m/demo-passkey ## Basic Information - **Project Name**: demo-passkey - **Description**: Passkey 学习实验室 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-20 - **Last Updated**: 2026-08-20 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Passkey 学习实验室 这是一个用于学习 Passkey / WebAuthn 完整流程的渐进式 Demo,覆盖环境检测、用户与 Challenge、真实凭据注册、无用户名登录、持久化、多账号会话和凭据管理。 ## 技术栈 - Java 21、Spring Boot 3.5 - Yubico WebAuthn Server 2.7 - Spring Data JPA、H2 文件数据库 - 原生 HTML、CSS、JavaScript - WebAuthn Level 2 浏览器 API 主要测试环境为 Windows 11、Chrome、Windows Hello 和 iPhone。Bitwarden 等第三方 Passkey 提供方也可以参与注册和登录,但其界面展示由提供方自行决定。 ## 快速开始 环境要求:Java 21、Maven 3.9+。 ```powershell mvn spring-boot:run ``` 使用 Chrome 打开 。 当前 RP ID 和 Origin 均配置为 `localhost`,不要改用 `127.0.0.1`。WebAuthn 凭据与 RP ID 绑定,两者不能混用。 运行自动化测试: ```powershell mvn test ``` ## 核心术语 | 名称 | 示例 | WebAuthn / Demo 含义 | | --- | --- | --- | | 账号名 | `liuyifei` | WebAuthn `user.name`,本站稳定的账号名称 | | 显示名称 | `刘亦菲` | WebAuthn `user.displayName`,认证器可以选择是否展示 | | Passkey 名称 | `我的 iPhone` | 本站为单个凭据保存的备注名称,不会同步到认证器 | | userHandle | Base64URL 随机值 | 与账号绑定的非公开稳定标识 | | credentialId | Base64URL 值 | 标识某一个 Passkey 的公钥凭据 | | Challenge | Base64URL 随机值 | 服务端签发的一次性随机数,用于防止响应重放 | | RP ID | `localhost` | 凭据所属的 relying party 域名范围 | 同一个账号可以拥有多个 Passkey。显示名称属于用户,Passkey 名称属于某一个具体凭据。 Bitwarden 通常显示 RP 名称和 `user.name`,不保证显示 `user.displayName`。WebAuthn 也没有提供网站修改 Bitwarden 条目名称的接口。 ## 完整流程 ### 注册 1. 服务端查找或创建用户,生成注册选项和一次性 Challenge。 2. 浏览器调用 `navigator.credentials.create()`。 3. Windows Hello、iPhone 或密码管理器在认证器内部生成密钥对。 4. 私钥留在认证器中,服务端永远不会收到私钥。 5. 服务端使用 Yubico 库校验客户端数据、Origin、RP ID 和认证器响应。 6. 校验成功后保存 credential ID、COSE 公钥和凭据元数据。 ### 登录 1. 服务端生成无用户名认证选项,不发送 `allowCredentials`。 2. 浏览器调用 `navigator.credentials.get()`。 3. 认证器根据 RP ID 找到可发现凭据,并让用户选择账号。 4. 认证器使用私钥签名 Challenge。 5. 服务端查找公钥并验证签名,成功后将账号加入 HttpSession。 ### 管理 1. Session 中只能管理已经完成 Passkey 认证的当前账号。 2. 服务端可重命名或删除公钥记录,但不能读取或删除设备私钥。 3. 删除账号会删除用户及其全部服务端公钥凭据。 4. Windows、iPhone 或 Bitwarden 中的 Passkey 需要在对应设备中单独清理。 ## 模块说明 ### 模块 01:环境检查 检查 WebAuthn API、安全上下文、平台认证器能力和服务端 RP 配置。本模块不会创建凭据,也不会打开认证器窗口。 手动验收: 1. WebAuthn API 显示“可用”。 2. 安全上下文显示“安全”。浏览器将 `http://localhost` 视为开发环境安全上下文。 3. 服务端配置显示“匹配”,RP ID 为 `localhost`。 4. 点击“重新检测”可以刷新结果。 5. iPhone 跨设备能力需要在真实注册时验证,无法提前确认。 ### 模块 02:用户与 Challenge Challenge 实验区用于观察认证器参与之前的服务端数据。 1. 输入账号名和显示名称,生成 Challenge。 2. 相同账号的 `userHandle` 保持不变,Challenge 每次都不同。 3. Challenge 只能消费一次,再次消费返回 `CHALLENGE_INVALID`。 4. Challenge 有效期为 5 分钟,未完成流程只保存在内存中。 ### 模块 03:真实 Passkey 注册 真实注册区调用 `navigator.credentials.create()`,Yubico 库负责验证认证器响应。 注册配置: - `residentKey: required` - `userVerification: required` - `attestation: none` - 注册超时:5 分钟 注册成功后页面展示 credential ID、COSE 公钥、AAGUID、备份状态和连接类型。用户和公钥凭据保存到 H2;未完成注册上下文仍只保存在内存中。 ### 模块 04:无用户名 Passkey 登录 登录时服务端省略 `allowCredentials`,由认证器根据 RP ID 展示可用账号。登录成功后服务端会: - 验证认证器签名和用户验证结果。 - 更新签名计数、备份状态和最后使用时间。 - 创建或更新 HttpSession。 - 将认证账号加入本次会话的账号列表。 认证 Challenge 有效期为 5 分钟且只能完成一次。 ### 模块 05:H2 持久化 用户和 Passkey 公钥凭据保存在 `data/passkey-demo.mv.db`,服务重启后仍可登录。Challenge、待完成注册和待完成登录保存在内存中,重启后失效。 开发环境可访问 : ```text JDBC URL: jdbc:h2:file:./data/passkey-demo;AUTO_SERVER=TRUE 用户名: sa 密码: 空 ``` 不要在生产环境开放 H2 Console。 ### 模块 06A:只读 Passkey 清单 “我的 Passkey”只展示当前账号的凭据,包括 Passkey 名称、credential ID、AAGUID、传输方式、安全状态、创建时间、最后使用时间和签名计数。账号名只从 HttpSession 获取,客户端不能通过查询参数查看其他账号。 ### 模块 06B:重命名与安全删除凭据 - Passkey 名称为 1 到 50 个字符。 - 旧数据名称为空时显示“未命名 Passkey”。 - 删除要求最近 10 分钟内完成过认证。 - 账号必须至少保留一个 Passkey。 - 删除只移除服务端公钥,不会删除设备私钥。 - 删除时使用数据库悲观锁,避免并发请求绕过最后一个凭据保护。 ### 模块 06C1:多账号登录与切换 同一浏览器 Session 可以保存多个已经认证的账号: 1. 点击“登录其他账号”并完成另一个账号的认证。 2. 每个账号独立记录认证时间和最近认证摘要。 3. 切换账号后,登录信息、注册目标和 Passkey 清单同步更新。 4. 页面分别展示“显示名称”和“账号名”。 5. “退出全部”清空 Session,但不会删除账号或 Passkey。 ### 模块 06C2:安全删除账号 - 必须输入当前账号名二次确认。 - 必须在最近 10 分钟内完成过该账号的认证。 - 用户和凭据在同一数据库事务中删除。 - 删除后自动切换到另一个已登录账号。 - 删除唯一已登录账号后回到未登录状态。 - 设备或 Bitwarden 中的 Passkey 不会被同步删除。 ### 模块 06D:安全追加 Passkey - 未登录时只能注册没有已登记凭据的新账号。 - 已有 Passkey 的账号必须先登录,防止攻击者植入新凭据。 - 认证器窗口取消后留下的 0 凭据账号允许重新注册。 - 已登录时服务端忽略前端账号名,始终为当前 Session 账号追加凭据。 - 添加凭据要求最近 10 分钟认证。 - 注册期间退出或切换账号会导致完成请求被拒绝。 - `excludeCredentials` 防止同一认证器重复登记已有凭据。 - 可填写最多 50 个字符的可选 Passkey 名称;未填写时为“未命名 Passkey”。 添加第二个 Passkey 时通常需要选择不同认证器。例如已有 Windows Hello 凭据时,可以选择 iPhone 或 Bitwarden。 ## REST API | 方法 | 地址 | 用途 | | --- | --- | --- | | `GET` | `/api/environment` | 读取 RP 配置和环境信息 | | `POST` | `/api/registration/challenge` | 创建教学用注册 Challenge | | `POST` | `/api/registration/challenge/consume` | 消费教学用 Challenge | | `POST` | `/api/passkeys/registration/start` | 开始真实 Passkey 注册 | | `POST` | `/api/passkeys/registration/finish` | 完成真实 Passkey 注册 | | `POST` | `/api/passkeys/authentication/start` | 开始无用户名认证 | | `POST` | `/api/passkeys/authentication/finish` | 验证签名并登录 | | `GET` | `/api/session` | 读取当前账号和已认证账号列表 | | `POST` | `/api/session/switch` | 切换到 Session 内已认证账号 | | `POST` | `/api/session/logout` | 退出全部账号 | | `GET` | `/api/passkeys` | 查询当前账号的 Passkey 清单 | | `PATCH` | `/api/passkeys/{credentialId}` | 重命名当前账号的凭据 | | `DELETE` | `/api/passkeys/{credentialId}` | 删除当前账号的凭据 | | `DELETE` | `/api/account` | 删除当前账号及全部服务端凭据 | 所有管理接口都以 HttpSession 为权限边界,不接受客户端指定目标账号。 ## 主要错误码 | 错误码 | 含义 | | --- | --- | | `CEREMONY_INVALID` | 注册或登录仪式不存在、已消费或已清除 | | `CEREMONY_EXPIRED` | 注册或登录仪式超过 5 分钟 | | `USERNAME_EXISTS` | 未登录时尝试为已有账号注册凭据 | | `REAUTHENTICATION_REQUIRED` | 敏感操作前的认证已经超过 10 分钟 | | `SESSION_ACCOUNT_CHANGED` | 注册过程中退出或切换了账号 | | `CREDENTIAL_EXISTS` | 相同凭据已经登记 | | `PASSKEY_NOT_FOUND` | 凭据不存在或不属于当前账号 | | `LAST_PASSKEY` | 尝试删除账号的最后一个 Passkey | | `NAME_INVALID` | Passkey 名称为空或超过长度限制 | | `CONFIRMATION_MISMATCH` | 删除账号时输入的确认账号名不匹配 | ## Windows Hello、iPhone 与 Bitwarden ### Windows Hello - 使用本机 PIN、指纹或面部验证。 - 凭据通常属于当前 Windows 环境。 - 同一账号重复注册时可能被 `excludeCredentials` 阻止。 ### iPhone - 在 Chrome 认证器选择界面选择使用其他设备。 - 使用 iPhone 扫描二维码,并通过 Face ID 或设备密码确认。 - 跨设备流程通常依赖蓝牙邻近验证。 ### Bitwarden - Bitwarden 可以作为 Passkey 提供方参与注册和登录。 - 列表通常显示 RP 名称与账号名 `user.name`。 - 不保证展示显示名称 `user.displayName`。 - 不会收到本站数据库中的 Passkey 备注名称。 ## 安全边界 本 Demo 已实现以下约束: - RP ID、Origin、Challenge 和认证器签名均由服务端校验。 - Challenge 一次性使用且有过期时间。 - 注册与登录要求用户验证。 - 敏感管理接口只信任 HttpSession 身份。 - 已有账号追加凭据需要先登录。 - 删除凭据和账号要求近期认证。 - 私钥永远不会发送到服务端。 本项目用于学习,不是可直接上线的身份系统。生产环境还需要 HTTPS、CSRF 防护、Session 集群方案、速率限制、审计日志、数据库迁移工具、账号恢复策略、可信代理配置、监控告警和更完整的异常处理。 ## 推荐手动验收 1. 注册新账号,并分别记录账号名、显示名称和 Passkey 名称。 2. 重启服务,确认无需重新注册即可登录。 3. 为同一账号使用另一认证器添加第二个 Passkey。 4. 重命名两个凭据并刷新页面。 5. 登录第二个账号,在两个账号之间切换并核对清单。 6. 删除其中一个 Passkey,确认另一个仍可登录。 7. 尝试删除最后一个 Passkey,确认被拒绝。 8. 使用测试账号验证账号删除和自动切换。 9. 在 Windows、iPhone 或 Bitwarden 中单独清理服务端已删除的残留 Passkey。