WebAuthn 与 Passkeys
这页适合作为“无密码登录方案入门与落地说明”。Passkeys 的优势很明显,但它不是“加个前端 API 就完事”,真正决定成败的是:挑战值管理、RP ID / Origin 一致性、凭证存储、回退登录方式和多设备体验。
什么是 Passkeys
Passkeys 是基于 WebAuthn 标准的无密码认证方案,使用公钥加密替代传统密码。
- 不可钓鱼:绑定域名,假网站无法获取凭证
- 无密码泄露风险:服务端只存公钥
- 跨设备同步:通过 iCloud Keychain / Google Password Manager
工作流程
注册:
1. 服务端生成 challenge
2. 浏览器调用 navigator.credentials.create()
3. 设备生成密钥对,用私钥签名 challenge
4. 服务端存储公钥和凭证 ID
登录:
1. 服务端生成 challenge
2. 浏览器调用 navigator.credentials.get()
3. 设备用私钥签名 challenge
4. 服务端用公钥验证签名
前端实现
注册
async function register(username: string) {
// 1. 从服务端获取注册选项
const options = await fetch("/api/auth/register-options", {
method: "POST",
body: JSON.stringify({ username }),
}).then((r) => r.json());
// 2. 创建凭证
const credential = (await navigator.credentials.create({
publicKey: {
challenge: base64ToBuffer(options.challenge),
rp: { name: "DomiVault", id: window.location.hostname },
user: {
id: base64ToBuffer(options.userId),
name: username,
displayName: username,
},
pubKeyCredParams: [
{ alg: -7, type: "public-key" }, // ES256
{ alg: -257, type: "public-key" }, // RS256
],
authenticatorSelection: {
residentKey: "required",
userVerification: "preferred",
},
timeout: 60000,
},
})) as PublicKeyCredential;
// 3. 发送到服务端验证
await fetch("/api/auth/register-verify", {
method: "POST",
body: JSON.stringify({
id: credential.id,
rawId: bufferToBase64(credential.rawId),
response: {
attestationObject: bufferToBase64(
(credential.response as AuthenticatorAttestationResponse)
.attestationObject,
),
clientDataJSON: bufferToBase64(credential.response.clientDataJSON),
},
}),
});
}
登录
async function login() {
const options = await fetch("/api/auth/login-options").then((r) => r.json());
const credential = (await navigator.credentials.get({
publicKey: {
challenge: base64ToBuffer(options.challenge),
rpId: window.location.hostname,
userVerification: "preferred",
timeout: 60000,
},
})) as PublicKeyCredential;
const result = await fetch("/api/auth/login-verify", {
method: "POST",
body: JSON.stringify({
id: credential.id,
rawId: bufferToBase64(credential.rawId),
response: {
authenticatorData: bufferToBase64(
(credential.response as AuthenticatorAssertionResponse)
.authenticatorData,
),
clientDataJSON: bufferToBase64(credential.response.clientDataJSON),
signature: bufferToBase64(
(credential.response as AuthenticatorAssertionResponse).signature,
),
},
}),
}).then((r) => r.json());
return result;
}
服务端(SimpleWebAuthn)
pnpm add @simplewebauthn/server @simplewebauthn/browser
import {
generateRegistrationOptions,
verifyRegistrationResponse,
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from "@simplewebauthn/server";
const rpName = "DomiVault";
const rpID = "example.com";
const origin = "https://example.com";
// 注册选项
const options = await generateRegistrationOptions({
rpName,
rpID,
userName: user.name,
attestationType: "none",
authenticatorSelection: {
residentKey: "required",
userVerification: "preferred",
},
});
// 验证注册
const verification = await verifyRegistrationResponse({
response: body,
expectedChallenge: challenge,
expectedOrigin: origin,
expectedRPID: rpID,
});
// 登录选项
const authOptions = await generateAuthenticationOptions({ rpID });
// 验证登录
const authVerification = await verifyAuthenticationResponse({
response: body,
expectedChallenge: challenge,
expectedOrigin: origin,
expectedRPID: rpID,
credential: {
id: storedCredential.id,
publicKey: storedCredential.publicKey,
counter: storedCredential.counter,
},
});
浏览器支持
所有现代浏览器均已支持:Chrome、Safari、Firefox、Edge。
// 检测支持
const supported = window.PublicKeyCredential !== undefined;
const platformSupported =
await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
const conditionalSupported =
await PublicKeyCredential.isConditionalMediationAvailable();
推荐落地顺序
如果你准备把 Passkeys 真接进业务,建议按下面顺序:
- 先梳理账号体系和现有登录流程
- 明确注册、绑定、登录、解绑、找回这几条链路
- 用成熟库先跑通最小可用版本
- 再补多设备、条件式 UI、回退登录和风控
不要一开始就自己手搓全部底层细节,成熟库会帮你避开很多编码和验证坑。
几个关键概念
- RP ID:通常是你的主域名或可控子域名范围
- Origin:必须与实际站点来源精确匹配
- Challenge:一次性随机挑战,必须短时有效、不可复用
- Credential ID / Public Key:服务端需要安全保存,用于后续验证
这几个概念只要有一个配置错,登录就很容易“看起来流程都对,但验证就是失败”。
体验与产品建议
- Passkeys 很适合作为主登录方式,但最好保留恢复和回退方案
- 多设备同步场景下,用户往往并不理解“凭证存在设备还是云同步里”,UI 文案要尽量直白
- 企业账号、共享设备、临时设备场景要特别注意登录确认与解绑流程
常见问题
本地能注册,线上不能登录
优先检查:
expectedOrigin是否和实际线上域名完全一致expectedRPID是否正确- challenge 是否被正确保存和比对
- 凭证 counter、publicKey、credential id 是否持久化正确
浏览器支持,但用户看不到创建入口
这通常和以下因素有关:
- 页面上下文不是安全来源(非 HTTPS)
- 平台或设备不支持当前认证器能力
- UI 没处理条件式中介或可用性检测结果
只上 Passkeys,会不会把用户锁在门外
有这个风险。比较稳妥的方式通常是:
- Passkeys 作为主登录方式
- 同时保留一条安全的恢复路径
- 对高风险操作再结合额外校验
延伸阅读
参考链接
- passkeys.dev — 开发者资源
- SimpleWebAuthn — JS 库
- WebAuthn Guide — 交互式指南
- Can I Use: WebAuthn — 兼容性