用一对钥匙替掉密码
passkey(通行密钥)不是「更安全的密码」,而是彻底换了模型——密码是两端都存的共享秘密(会被钓走、撞库、泄库),passkey 是一对非对称密钥:私钥被锁在你设备的 authenticator 里永不离开,服务器只存公钥;登录靠对服务端发来的
challenge 做一次签名来证明「我手里有私钥」。底层是 FIDO2 = WebAuthn(浏览器 JS API navigator.credentials)+ CTAP2(浏览器↔认证器协议)。
本页每节都直接调浏览器原生 PublicKeyCredential / navigator.credentials——能力检测是当场跑出来的真实结果,「真机尝试」会触发本机 Touch ID / Windows Hello(localhost 是 secure context,WebAuthn 可用)。四节:密码的缺陷与支持 →
注册一把新钥匙 → 三个正交的旋钮 → 用钥匙登录。
1 · 密码的根本缺陷,与 passkey 的不同模型
密码的根本问题不在「不够长」,而在它是共享秘密(shared secret):同一个值你和服务器两端都要存。于是它能被钓鱼页面骗走、能在 A 站泄露后拿去对 B 站撞库、服务器一旦被脱库就整批泄露。passkey 换了一套数学:非对称密钥对——私钥锁在你设备里永不外传,服务器只拿到公钥,就算泄库也没有可冒用的秘密。
1.1 · 共享秘密 vs 非对称密钥
抗钓鱼是「天生」的,不靠用户警惕。 每个 passkey 在创建时就绑定了 rp.id(域名)。浏览器只会把 example.com 的凭证用在 example.com 上——你被骗到 examp1e.com 的钓鱼站,那里的 JS 根本调不出你
example.com 的 passkey。密码做不到这点:再像的假站,用户仍可能手动输入密码。
1.2 · FIDO / WebAuthn / CTAP 是什么关系
这几个词常被混用,其实是一摞分层标准,由 FIDO Alliance(Fast IDentity Online)与 W3C 共同制定。「passkey」是这套技术面向用户的品牌名:
| 层 | 名字 | 管什么 |
|---|---|---|
| 伞 | FIDO2 | 总称 = WebAuthn + CTAP2。passkey 就是 FIDO2 凭证「可发现 + 可同步」时的用户叫法。 |
| 网页 ↔ 浏览器 | WebAuthn | W3C 的浏览器 JS API:navigator.credentials.create() / .get() 与 PublicKeyCredential。本系列讲的正是它。 |
| 浏览器 ↔ 认证器 | CTAP2 | Client to Authenticator Protocol:浏览器怎么和外接安全钥 / 手机(USB·NFC·BLE)对话。网页层看不到,但「扫码用手机登录电脑」靠它。 |
1.3 · 你这台机器现在支持吗(当场检测)
下面四项都是真的调浏览器 API 跑出来的。WebAuthn 要求 secure context(HTTPS 或 localhost);isUserVerifyingPlatformAuthenticatorAvailable() 告诉你本机有没有内建认证器(Touch ID / Windows Hello);isConditionalMediationAvailable()
决定能不能做表单自动填充式登录。
1.4 · 跨平台支持矩阵
passkey 已是各大平台默认能力,差异主要在「能不能跨设备同步」。数据概览自 passkeys.dev(FIDO Alliance 维护):
| 平台 | 创建 passkey | 云端同步 | 当 cross-device 认证器(扫码) |
|---|---|---|---|
| Apple(iOS 16+ / macOS 13+) | ✓ | iCloud Keychain | ✓ |
| Android(9+ / Play services) | ✓ | Google Password Manager | ✓ |
| Windows(10/11 · Hello) | ✓ | 11 起逐步同步 | ✓ |
| Chrome / Edge / Safari / Firefox | ✓ | 借底层 OS / 密码管理器 | ✓ |
| 硬件安全钥(YubiKey 等) | ✓ | ✗(不同步,绑在钥上) | 本身就是 cross-platform |
1.5 · 在 Chrome 里管理已有 passkey
想看 / 删本机存了哪些 passkey,Chrome 的入口层级较深(得先用 passkey 登录过一次,管理页才会出现):
地址栏直接输入 chrome://settings/passkeys 也能到。手机上则在系统设置里:iOS =「密码」、Android =「Google 密码管理工具」。删掉本机 passkey 不会注销服务器侧的公钥——那是两份独立的东西,服务器侧要去各站「安全设置」里单独移除。
2 · 注册一把新钥匙
注册(在 WebAuthn 里叫 attestation / registration)做的事:让 authenticator 当场生成一对新密钥,把公钥连同一个新 credential id 交回服务器存档,私钥留在设备里。入口是 navigator.credentials.create({ publicKey })——关键全在
publicKey 这个 PublicKeyCredentialCreationOptions 里。
2.1 · 一次注册握手
2.2 · 拼一份 creation options(改开关看 JSON)
下面几个开关实时改写 publicKey。把鼠标移到某个字段说明上,JSON 里对应那行会点亮:
三个旋钮 authenticatorSelection 这节先用默认值。 它里面的 authenticatorAttachment / residentKey / userVerification 是三件互相独立的事,单独成一节讲——见 三个正交的旋钮。
2.3 · 真机尝试:在这台机器上真注册一把
点下面的按钮会用上面的选项真的调 create()——本机有 Touch ID / Windows Hello 的话会弹出生物识别。rp.id 取当前域名,challenge 用 crypto.getRandomValues 现场生成。成功后凭证 id 会被存进 localStorage,用钥匙登录一节能直接拿去登录。
为什么 challenge / user.id 是 BufferSource 而不是字符串? WebAuthn 选项里凡是「二进制数据」(challenge、id)都用 ArrayBuffer / Uint8Array。服务端通常发 base64url 字符串,前端要先 decode 成
BufferSource 再塞进选项——新接口 PublicKeyCredential.parseCreationOptionsFromJSON() 能帮你自动转。
3 · 三个最容易混淆的旋钮
注册时
authenticatorSelection 里有三个字段,初学者几乎都会把它们当成「一件事的不同说法」。其实它们互相独立、各自调节——回答的是三个不同问题:认证器在哪、凭证存哪、要不要验人。下面三个旋钮可任意组合,看每种组合意味着怎样的登录体验。
3.1 · authenticatorAttachment——认证器在哪
控制让用户用「什么位置」的认证器。注意它不决定验不验人,也不决定凭证存哪:
| 取值 | 含义 | 例子 |
|---|---|---|
platform |
绑在本机的内建认证器,不可拔。 | Touch ID / Face ID / Windows Hello / Android 指纹 |
cross-platform |
可移动、能接入其他设备的认证器。 | USB / NFC 安全钥(YubiKey)、扫码借用的另一台手机 |
| (不填) | 两者都允许,把选择权交给用户。 | 大多数面向公众的网站不填 |
3.2 · residentKey / discoverable credential——凭证存哪
决定凭证是不是 discoverable credential(可发现凭证,旧称 resident key)——即把凭证本体存进 authenticator、并且能在不被告知 id 的情况下被列举出来。它直接决定登录能不能免输用户名:
| 取值 | 凭证存在哪 | 对登录的影响 |
|---|---|---|
required |
存进 authenticator(占一个槽位)。 | 可发现 → 登录时 allowCredentials 可留空,authenticator 自己列出账号 → usernameless。passkey 的典型选择。 |
preferred |
能存就存(看 authenticator)。 | 尽量做成可发现,设备不支持则退回不可发现。 |
discouraged |
不存本体,只在服务器存「加密包」(server-side credential)。 | 不可发现 → 登录前服务器必须按用户名查出 credential id,放进 allowCredentials 递给浏览器。 |
「可发现」到底发现了什么? discoverable credential 在 authenticator 里连带存了 rp.id 与 user 信息,所以浏览器能问它「你有哪些 example.com 的账号」并列出供用户选择——这就是为什么能不先报用户名就登录。代价是它占用 authenticator
的存储槽(硬件密钥的槽位有限)。
3.3 · userVerification——要不要验人(安全等级 LoA / AAL)
控制 authenticator 要不要额外证明「现在按的人就是设备主人」(生物识别 / PIN)。它是安全等级的旋钮——决定这次认证算「单因子(只证明握有设备)」还是「双因子(握有设备 + 通过验证)」,和凭证存哪完全无关:
| 取值 | 要求 | 得到的保证 |
|---|---|---|
required |
必须验人(指纹 / 面容 / PIN),不通过就失败。 | 双因子:掌握设备 + 验证本人。passkey 默认期望。 |
preferred |
能验就验,不能验也放行。 | 视设备而定(默认值)。 |
discouraged |
只验在场(碰一下 user presence),不验身份。 | 单因子。老式 U2F 二次因子安全钥常这样。 |
正交性一句话:「验不验人(userVerification)」属于安全等级 / AAL;「凭证存不存得进设备(residentKey)」属于存储与可发现性;「认证器在本机还是外接(authenticatorAttachment)」属于物理位置。三件事可以任意搭配——上面三个旋钮就是用于验证这一点。
3.4 · 几种典型组合
| 场景 | attachment | residentKey | userVerification |
|---|---|---|---|
| 现代 passkey(本机免密登录) | platform |
required |
required |
| 面向公众、不限设备 | (不填) | preferred |
preferred |
| 老式 U2F 二次因子(密码之外再加一把安全钥) | cross-platform |
discouraged |
discouraged |
| 高保证(企业 / 金融) | cross-platform |
required |
required |
4 · 用钥匙登录
登录(WebAuthn 里叫 assertion)做的事:服务器发一个新 challenge,authenticator 用已有私钥给它签个名,服务器用注册时存的公钥验签。入口是
navigator.credentials.get({ publicKey })——全程没有任何秘密被发送,只发一个签名。
4.1 · 一次登录握手
4.2 · 拼一份 request options
把鼠标移到某个字段说明上,JSON 里对应那行会点亮:
allowCredentials 留空 = usernameless 登录的关键。 留空时浏览器去问 authenticator「你存了哪些本站的 discoverable credential」,直接列给用户挑——用户不用先输用户名。填了列表则是「先知道你是谁,再让你证明」的流程(凭证可以是 non-discoverable 的 server-side
credential)。
旧设备的限制: 部分老 authenticator 不支持空 allowCredentials,直接抛出 Use of an empty 'allowCredentials' list is not supported on this device。想兼容这类设备就得回落到「先收用户名 → 服务器查出 id → 填进 allowCredentials」的传统流程,或用 isConditionalMediationAvailable() 等检测做能力分支。
4.3 · 真机尝试:用注册一节创建的钥匙登录
先到注册一节建一把(凭证 id 会存进 localStorage),回到这里点登录就会触发本机生物识别、真的走一遍 get() 并验出一个 signature。allowCredentials 选「空」则改走 discoverable 流程。
4.4 · Conditional UI:把 passkey 接进输入框自动填充
体验最顺畅的登录不需要点任何按钮:页面一加载就以 mediation:'conditional' 在后台挂一个 get(),配合输入框上的 autocomplete="username webauthn",用户点输入框时系统下拉里直接列出 passkey,选了就登录。它不会主动弹模态框,没有可用凭证时悄无声息:
4.5 · 跨设备:hybrid(扫码用手机登录电脑)
电脑上没存 passkey 时,可借手机里的那把:浏览器弹一个二维码,手机扫码后通过蓝牙就近确认身份——这就是 hybrid transport(旧称 caBLE),底层走 CTAP2。代码侧只要在 get() 里给 hints: ['hybrid'] 把它提到优先,或在
allowCredentials 的条目里标 transports: ['hybrid']。私钥始终不离开手机,电脑只拿到一次签名。
相关链接
-
Web Authentication: An API for accessing Public Key Credentials
W3C
WebAuthn Level 3 规范:
PublicKeyCredential接口、create / get 的全部选项(authenticatorSelection、attestation、hints…)与算法的权威定义。 -
MDN · Web Authentication API
developer.mozilla.org
WebAuthn 总览:注册 / 登录两条流程、discoverable credential、autofill UI 的概念图与
navigator.credentials.create()/.get()的整体用法。 -
MDN · PublicKeyCredentialCreationOptions
developer.mozilla.org
对应「注册」「三个旋钮」两节:
rp/user/challenge/pubKeyCredParams/authenticatorSelection/attestation/excludeCredentials逐字段说明与兼容性。 - Can I use · Web Authentication API caniuse.com WebAuthn 的实时浏览器支持矩阵(逐版本细分),与本系列「当场检测」「跨平台支持」两节对照。
-
Create a passkey for passwordless logins
web.dev
对应「注册」一节:服务端发选项 → 前端
create()→ 回传公钥的完整握手,含excludeCredentials防重复注册的用法。 -
Sign in with a passkey through form autofill
web.dev
对应「登录」一节:Conditional UI / form autofill ——
mediation:'conditional'配合autocomplete="username webauthn"把 passkey 接进原生输入框建议。 -
Passkey form autofill (Conditional UI)
developer.chrome.com
Chrome 侧的 Conditional UI 落地细节与
isConditionalMediationAvailable()检测。 -
Discoverable credentials deep dive
web.dev / FIDO
对应「三个旋钮」一节:discoverable credential(resident key)到底存了什么、为何能 usernameless,以及
residentKey三档取值。 - Passkey device support passkeys.dev 各 OS / 浏览器 / authenticator 的 passkey 支持矩阵(本系列「支持」表的数据来源);站点 passkeys.dev 由 FIDO Alliance 维护。
- Passkeys demo Google Google 官方可交互 demo:真实注册与登录的完整流程,可与本系列的「真机尝试」对照。
- Passwords suck. Can passkeys replace them? kerkour.com 对应「密码的缺陷」一节:从工程视角讲密码的系统性缺陷,以及 passkey 在抗钓鱼 / 抗泄库上的根本不同与现实阻力。