能力 · API
WebAuthn 与通行密钥(passkeys)
发布于
Web Authentication API 通过 navigator.credentials.create({ publicKey }) 注册一个绑定到认证器的公钥凭据,之后用 navigator.credentials.get({ publicKey }) 证明持有它,于是 PWA 可以让用户用指纹、人脸或设备 PIN 登录,而不是密码。通行密钥(passkey)就是以可发现方式(residentKey: "required")创建的这类凭据,在多数平台上由 iCloud 钥匙串或 Google 密码管理器等凭据管理器同步;签名数据包含源与一次性 challenge,所以在仿冒域名上钓到的凭据无法使用。
PublicKeyCredential 在 Chrome 67、Edge 18、Firefox 60 与 Safari 13 发布;条件式中介(通行密钥自动填充)在 Chrome 108、Safari 16 与 Firefox 119 可用;PublicKeyCredential.getClientCapabilities() 在 Chrome 133、Safari 17.4 与 Firefox 135 可用(BCD api.PublicKeyCredential)。Android WebView 131 暴露 isConditionalMediationAvailable(),但由于缺少自动填充集成,它兑现为 false。各平台的通行密钥同步覆盖情况见 passkeys.dev。
navigator.credentials.create({ publicKey: creationOptions, signal })navigator.credentials.get({ publicKey: requestOptions, mediation, signal })
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()PublicKeyCredential.isConditionalMediationAvailable()PublicKeyCredential.getClientCapabilities()PublicKeyCredential.parseCreationOptionsFromJSON(json)PublicKeyCredential.parseRequestOptionsFromJSON(json)credential.toJSON()create() 以一个 PublicKeyCredential 兑现,其 response 是 AuthenticatorAttestationResponse(clientDataJSON、attestationObject、getPublicKey()、getTransports());get() 兑现的凭据其 response 是 AuthenticatorAssertionResponse(clientDataJSON、authenticatorData、signature、userHandle)。静态的 is…Available() 方法以布尔值兑现,不需要手势。两个仪式都要求安全上下文;跨源 iframe 还需要 publickey-credentials-create 或 publickey-credentials-get Permissions Policy 特性。
create() 在 publicKey 下接受 PublicKeyCredentialCreationOptions;get() 接受 PublicKeyCredentialRequestOptions,再加上外层 CredentialRequestOptions 的 mediation 成员。二进制成员的类型是 BufferSource;JSON 变体接受 base64url 字符串。
| 字典 | 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| 创建 | rp |
PublicKeyCredentialRpEntity |
是 | name(必填)与 id(默认为源的有效域名,必须是它的可注册后缀)。 |
| 创建 | user |
PublicKeyCredentialUserEntity |
是 | id(1 到 64 字节,不透明)、name、displayName。 |
| 创建 | challenge |
BufferSource |
是 | 服务端生成的随机字节,回显在 clientDataJSON 中。 |
| 创建 | pubKeyCredParams |
sequence<PublicKeyCredentialParameters> |
是 | { type: "public-key", alg } 条目;-7(ES256)与 -257(RS256)覆盖当前的认证器。 |
| 创建 | timeout |
unsigned long |
否 | 毫秒提示值,客户端可能裁剪。 |
| 创建 | excludeCredentials |
sequence<PublicKeyCredentialDescriptor> |
否 | 用户已有的凭据 ID;匹配到的认证器以 InvalidStateError 拒绝。 |
| 创建 | authenticatorSelection |
AuthenticatorSelectionCriteria |
否 | authenticatorAttachment("platform" 或 "cross-platform")、residentKey("discouraged"、"preferred"、"required")、requireResidentKey(遗留布尔值)、userVerification("required"、默认 "preferred"、"discouraged")。 |
| 创建 | hints |
sequence<DOMString> |
否 | "security-key"、"client-device"、"hybrid",按偏好排序。 |
| 创建 | attestation |
DOMString |
否,默认 "none" |
"none"、"indirect"、"direct"、"enterprise"。 |
| 创建 | extensions |
AuthenticationExtensionsClientInputs |
否 | 例如 credProps,用于得知凭据是否成为可发现凭据。 |
| 请求 | challenge |
BufferSource |
是 | 新的服务端 challenge。 |
| 请求 | rpId |
DOMString |
否 | 必须与创建时使用的 rp.id 一致。 |
| 请求 | allowCredentials |
sequence<PublicKeyCredentialDescriptor> |
否 | 留空让认证器自行选择可发现凭据(通行密钥流程)。 |
| 请求 | userVerification |
DOMString |
否,默认 "preferred" |
同上。 |
| 请求 | timeout、hints、extensions |
否 | 与创建时相同。 | |
| 外层 | mediation |
CredentialMediationRequirement |
否,默认 "optional" |
"conditional" 在表单自动填充中展示通行密钥并静默等待;"required" 强制弹窗;"silent" 对 publicKey 会被拒绝。 |
| 外层 | signal |
AbortSignal |
否 | 中止后,进行中的仪式以 AbortError 拒绝。 |
| 异常 | 条件 |
|---|---|
NotAllowedError |
调用方的源是不透明源;文档与某个祖先跨源且缺少对应的 Permissions Policy 特性;客户端要求瞬时激活而调用时没有;用户拒绝、超时,或找不到可用凭据且用户关闭了对话框;或(create())用户代理近期没有中介过认证且不授予同意。 |
SecurityError |
有效域名不是合法域名,或 rp.id / rpId 既不等于它也不是它的可注册后缀,且关联源检查(/.well-known/webauthn)失败。 |
TypeError |
user.id 不在 1 到 64 字节之间。 |
NotSupportedError |
pubKeyCredParams 中没有任何一项的类型与算法被客户端支持。 |
InvalidStateError |
某个认证器持有 excludeCredentials 中列出的凭据且用户已同意(这是客户端唯一原样上抛的认证器错误)。 |
AbortError |
signal 已中止。 |
EncodingError |
parseCreationOptionsFromJSON() 或 parseRequestOptionsFromJSON() 遇到无法解码的值。 |
InvalidStateError 以外的认证器级状态(认证器没有可发现凭据存储以满足 residentKey: "required"、或不具备用户验证能力时的 ConstraintError,内部失败时的 UnknownError)不会被规范的客户端算法上抛;仪式继续尝试其他认证器,最终在超时时以 NotAllowedError 结束。Chrome 与此不同,会直接报告这些状态(见实测)。
每个示例都先检测 PublicKeyCredential,并给出在没有通行密钥的环境里保留密码登录的分支。challenge、用户 ID 与凭据 ID 由服务端以 base64url 字符串下发,浏览器有 JSON 辅助方法时直接交给它们处理。
注册通行密钥,缺少支持时回退到密码表单
Section titled “注册通行密钥,缺少支持时回退到密码表单”向服务端请求创建选项,确认存在平台认证器,再把证明(attestation)回传。Chrome 129、Firefox 119 与 Safari 18.4 之前的浏览器没有 parseCreationOptionsFromJSON(),在那里手动解码 base64url。
const fromB64url = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
async function registerPasskey() { if (!window.PublicKeyCredential) return showPasswordForm('当前浏览器不支持通行密钥。'); if (!(await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable())) { return showPasswordForm('此设备没有平台认证器。'); }
const json = await (await fetch('/webauthn/register/options', { method: 'POST' })).json(); const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON ? PublicKeyCredential.parseCreationOptionsFromJSON(json) : { ...json, challenge: fromB64url(json.challenge), user: { ...json.user, id: fromB64url(json.user.id) }, excludeCredentials: (json.excludeCredentials ?? []).map((c) => ({ ...c, id: fromB64url(c.id) })), };
try { const credential = await navigator.credentials.create({ publicKey }); await fetch('/webauthn/register/verify', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(credential.toJSON ? credential.toJSON() : serialize(credential)), }); } catch (err) { if (err.name === 'InvalidStateError') return notify('此设备上已有该账号的通行密钥。'); if (err.name === 'NotAllowedError') return notify('已取消创建通行密钥。'); throw err; }}服务端的选项应设置 authenticatorSelection: { residentKey: "required", userVerification: "preferred" },凭据才是可发现的;否则用户必须先输入用户名,get() 才找得到它。
用通行密钥自动填充登录,并保留弹窗回退
Section titled “用通行密钥自动填充登录,并保留弹窗回退”mediation: "conditional" 让浏览器把通行密钥列在用户名输入框的自动填充里。请求会一直等到用户选中,所以在页面加载时就发起,并用一个 AbortSignal 配合可见的「用通行密钥登录」按钮:按钮先取消它,再发起弹窗请求。
async function startConditionalSignIn(input, button) { if (!window.PublicKeyCredential?.isConditionalMediationAvailable) return; if (!(await PublicKeyCredential.isConditionalMediationAvailable())) return;
input.setAttribute('autocomplete', 'username webauthn'); const controller = new AbortController(); const options = await (await fetch('/webauthn/login/options')).json(); const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(options);
button.addEventListener('click', async () => { controller.abort(); const assertion = await navigator.credentials.get({ publicKey, mediation: 'required' }); await verify(assertion); });
try { const assertion = await navigator.credentials.get({ publicKey, mediation: 'conditional', signal: controller.signal }); await verify(assertion); } catch (err) { if (err.name !== 'AbortError') console.error(`${err.name}: ${err.message}`); }}把 allowCredentials 留空,这样为 rpId 保存的任何通行密钥都能出现;没有通行密钥的用户看到的是普通自动填充,密码路径不受影响。
在平台通行密钥与安全密钥之间取舍
Section titled “在平台通行密钥与安全密钥之间取舍”hints 告诉浏览器优先展示哪种界面。给员工配发硬件密钥的企业应用,以 "security-key" 开头并要求用户验证;面向消费者的应用用 "client-device",让设备自带的生物识别排在最前。
async function createFor(audience, publicKey) { if (!window.PublicKeyCredential) throw new Error('WebAuthn 不可用'); const options = { ...publicKey, hints: audience === 'workforce' ? ['security-key', 'hybrid'] : ['client-device', 'hybrid'], authenticatorSelection: { residentKey: 'required', userVerification: audience === 'workforce' ? 'required' : 'preferred', }, }; return navigator.credentials.create({ publicKey: options });}不支持 hints 的浏览器按 WebIDL 字典规则丢弃该成员,回退是自动的:显示默认选择器。
- Credential Management API(凭据管理接口),这些调用所在的容器
- iOS 与 Safari 上的 PWA,通行密钥在那里经 iCloud 钥匙串同步
- Web Authentication Level 3: create a new credential(w3.org)
- Web Authentication Level 3: get an assertion(w3.org)
- Create a passkey for passwordless logins(web.dev)
- Sign in with a passkey through form autofill(web.dev)
- Device support for passkeys(passkeys.dev)
规范
| 规范 | 状态 |
|---|---|
| Web Authentication Level 3: create a new credential | W3C |
| Web Authentication Level 3: get an assertion | W3C |
| Web Authentication Level 3: AuthenticatorSelectionCriteria | W3C |