Appearance
结论速答
- 浏览器不会把“品牌/提供方”直接告诉前端(这是刻意的隐私设计)。你无法仅凭
navigator.credentials.get()的返回结果直接断言“用户点的是 Google、Windows Hello、Android 手机还是 Apple 钥匙串”。(W3C) - 你能拿到的是认证器类别:
credential.authenticatorAttachment→"platform"(平台,如 Windows Hello/Touch ID/Android 平台密钥)或"cross-platform"(漫游,如安全钥/YubiKey,或手机经 Hybrid/蓝牙/扫码)。但它不是品牌级别。(MDN Web Docs) - 如需在 UI 中展示“来自 Google / Apple / Windows Hello”等,正确做法是:在注册时记录 AAGUID → 服务端映射到提供方;认证时根据
rawId找回该凭据的“提供方标签”并返回给前端显示。注意:移动端大多用 none attestation,AAGUID 仅用于展示,不应作为安全决策。(web.dev)
能拿到的前端信号
1) 认证器“类别”
credential.authenticatorAttachment:"platform"/"cross-platform"。仅代表本机平台还是漫游/跨设备,不能细化到“Google/Apple/Windows”。(MDN Web Docs)
例:若你想在“跨设备登录”后建议用户把凭据保存到本机,可用它来判断(Chrome 指南里就是这么建议的)。(web.dev)
2) 扩展结果(可选)
credential.getClientExtensionResults():如你在publicKey.extensions里请求了扩展,这里会返回结果对象。它可能用于风险/设备判别(如devicePubKey帮你识别具体使用的那台设备),但仍不揭示“品牌”。(MDN Web Docs)
3) 客户端能力/提示(非识别)
PublicKeyCredential.getClientCapabilities()可让你探知是否支持 hybrid transport / 条件式自动填充等,以便调整 UI,但不是“用的是谁家”。(MDN Web Docs)
想显示“Google/Apple/Windows”的正确方案(服务端主导)
A. 注册时:记录 AAGUID → 映射“提供方”
- AAGUID 是“认证器型号/提供方”的 UUID。注册时(
create())在服务端解析并保存到凭据表,后续用于 UI 文案(例如“由 Google 密码管理器创建”)。官方示例与社区清单可直接映射 AAGUID → 提供方名称/图标。注意:无签名背书的场景仅用于展示。(web.dev)
复杂逻辑:提取 AAGUID 并存库
ts// 上一行:SimpleWebAuthn 注册校验后,拿到 AAGUID 并持久化(仅展示用途) const { verified, registrationInfo } = await verifyRegistrationResponse(...); const aaguid = registrationInfo?.aaguid; // 可能为 '00000000-0000-0000-0000-000000000000' // 上一行:用官方推荐的社区清单将 AAGUID → 提供方标签(Google/Apple/…) const provider = mapAAGUIDToProvider(aaguid); // 例:'Google Password Manager' | 'iCloud Keychain' | ... await db.credentials.upsert({ credentialId, userId, aaguid, providerLabel: provider });资料:如何用 AAGUID 判定提供方 & 社区清单链接(web.dev 文章)。(web.dev)
B. 认证时:凭 rawId 找回“提供方标签”
- 登录
get()成功后,前端把rawId发回;服务端校验通过后,根据rawId查询凭据表,把保存的providerLabel回传给前端用于 UI 展示。 - SimpleWebAuthn 还会告诉你凭据类型/是否多设备(
credentialDeviceType/credentialBackedUp),可做补充标签(例如“多设备同步的 passkey”)。(简单网页认证)
仅做“类别”与“跨设备”判断时(无需品牌)
只改你现有前端必要行(按你的要求“只给改动”):
ts
// 上一行:读取认证器类别(platform/cross-platform),随 payload 发回,便于统计/风控/UI
const attachment = (cred as PublicKeyCredential).authenticatorAttachment as
| "platform"
| "cross-platform"
| null;
// 上一行:如你启用了扩展,这里拿到客户端扩展结果(可能含 devicePubKey 等),可一并上传分析
const clientExt = (cred as PublicKeyCredential).getClientExtensionResults?.();
// 上一行:把两个“提示性”字段附在 payload 里(非安全必需,仅用于展示/分析)
payload.meta = {
authenticatorAttachment: attachment,
clientExtensionResults: clientExt,
};为什么不能直接知道“Google / Apple / Windows Hello”
- 规范与浏览器刻意最小化披露:避免把用户的设备/账户生态暴露给站点(隐私原则)。API 仅给“类别”(platform/cross-platform),不会告诉你“是 Google 还是 Apple”。(W3C)
- AAGUID 仅在注册态可得且常为匿名化:很多平台默认
none attestation,即便有 AAGUID,也只适合做 UI 标签,不应做强安全策略。(web.dev)
进阶:区分“手机(Hybrid)” vs “U 盘钥匙”
严格来说,结果里也不直接告诉你。
"cross-platform"既可能是 安全钥(USB/NFC/BLE),也可能是手机经 Hybrid。(MDN Web Docs)可行的推断思路(仍非 100% 准确):
- 检查客户端能力是否支持
hybridTransport(仅做 UI/流程分支)。(MDN Web Docs) - 在请求里开启
devicePubKey扩展并在服务端校验其输出,可用于识别“这次是同一台手机/设备”(设备指纹化的合规替代),但仍不是品牌信息。(GitHub)
- 检查客户端能力是否支持
小抄:你可以立刻加的最小改动
前端(认证)——只贴需新增行:
ts
// 上一行:把 authenticatorAttachment 带回去,便于服务端记录这次是平台还是漫游
const attachment =
(cred as PublicKeyCredential).authenticatorAttachment ?? null;
// 上一行:如你要研究扩展结果(例如 devicePubKey),连同其他字段一起上传
const ext = (cred as PublicKeyCredential).getClientExtensionResults?.() ?? {};
await loginByCredential({ ...payload, meta: { attachment, ext } });服务端(注册验证后)——只贴需新增行:
ts
// 上一行:注册成功后,提取 AAGUID 并映射“提供方标签”(仅显示用途)
const { aaguid } = verification.registrationInfo!;
const providerLabel = mapAAGUIDToProvider(aaguid); // 例:'Google Password Manager' | 'iCloud Keychain' | 'Windows Hello' | 'YubiKey' | 'Unknown'
await saveCredential({ credentialId, userId, aaguid, providerLabel });服务端(认证成功后)——只贴需新增行:
ts
// 上一行:用 rawId 找回凭据,连同 providerLabel 返回前端做 UI 展示
const cred = await getCredentialById(credentialId);
return { ok: true, providerLabel: cred?.providerLabel ?? "Unknown" };参考
authenticatorAttachment仅提供类别(platform / cross-platform),不包含品牌。(MDN Web Docs)- 用 AAGUID 在注册时标注“哪个提供方创建了该 passkey”(仅用于展示;无背书时不要做安全决策)。(web.dev)
- WebAuthn 的隐私/最小披露模型(无法直接知道品牌)。(W3C)
- 客户端能力探测(hybridTransport/conditional UI 等)。(MDN Web Docs)
- SimpleWebAuthn:注册/认证与 multiDevice/credentialBackedUp 语义,用于补充标签。(简单网页认证)
需要的话,我可以顺手给你一份AAGUID→ 提供方的 JSON 映射样例(含 Google/Apple/YubiKey/Windows Hello 的常见 AAGUID),以及注册环节如何在 none attestation 下做“仅展示用途”的安全说明。
好嘞,给你一套可直接用的 AAGUID → 提供方映射样例(JSON),再配后端最小改动片段(注册提取并存 AAGUID、登录返回 providerLabel)。我只给必要片段,复杂逻辑在上一行写中文注释,符合你的习惯。
1) AAGUID → 提供方映射(建议放到 aaguid.providers.ts)
说明:这只是用于 UI 展示的“友好标签”,不要据此做安全决策;Apple/Safari 常见全 0 AAGUID,无法唯一指向 Apple。Google/Windows/YubiKey 取值参考了权威与社区清单(见文末引用)。
ts
// 上一行:AAGUID → 人类可读标签(仅展示用途)
export const AAGUID_PROVIDER_MAP: Record<string, string> = {
// Google Password Manager(Chrome/Android 等)
"ea9b8d66-4d01-1d21-3ce4-b6b48cb575d4": "Google Password Manager", // :contentReference[oaicite:0]{index=0}
// Windows Hello(多种形态)
"6028b017-b1d4-4c02-b4b3-afcdafc96bb2": "Windows Hello (Software)", // :contentReference[oaicite:1]{index=1}
"9ddd1817-af5a-4672-a2b9-3e3dd95000a9": "Windows Hello (VBS Hardware)", // :contentReference[oaicite:2]{index=2}
"08987058-cadc-4b81-b6e1-30de50dcbe96": "Windows Hello (Hardware)", // :contentReference[oaicite:3]{index=3}
// Apple iCloud Keychain:常见 none attestation → 全 0(仅提示用,不可靠)
"00000000-0000-0000-0000-000000000000":
"Unknown (可能是 iCloud Keychain / none attestation)", // :contentReference[oaicite:4]{index=4}
// YubiKey(示例,型号/固件不同对应不同 AAGUID;更多见官方表)
"ee882879-721c-4913-9775-3dfcce97072a": "YubiKey 5 (USB-A/5.2/5.4)", // :contentReference[oaicite:5]{index=5}
"2fc0579f-8113-47ea-b116-bb5a8db9202a": "YubiKey 5 NFC / 5C NFC (5.2/5.4)", // :contentReference[oaicite:6]{index=6}
"fa2b99dc-9e39-4257-8f92-4a30d23c4118": "YubiKey 5 NFC (5.1)", // :contentReference[oaicite:7]{index=7}
"f8a011f3-8c0a-4d15-8006-17111f9edc7": "Security Key by Yubico (Blue/5.1)", // :contentReference[oaicite:8]{index=8}
};可选增强:你也可以在启动时拉取社区维护清单(
passkeydeveloper/passkey-authenticator-aaguids)缓存合并,用于展示;该清单不保证长期可用且不是安全来源。(GitHub)
2) 映射函数(空/未知兜底)
ts
// 上一行:把 AAGUID 映射成“展示用标签”,未知或空值兜底
export function mapAAGUIDToProvider(aaguid?: string | null): string {
if (!aaguid) return "Unknown";
const key = aaguid.toLowerCase();
return AAGUID_PROVIDER_MAP[key] ?? "Unknown";
}3) 注册成功后,提取并持久化 AAGUID(NestJS + simplewebauthn v11+)
ts
// 上一行:注册校验成功后,提取 aaguid,仅用于 UI 展示(不要做安全准入)
const { verified, registrationInfo } = await verifyRegistrationResponse(params);
if (!verified || !registrationInfo)
throw new UnauthorizedException("Invalid attestation");
const aaguid = registrationInfo.aaguid ?? null;
const providerLabel = mapAAGUIDToProvider(aaguid);
// 上一行:把 aaguid / providerLabel 和 credentialId、公钥、counter 等一起存库
await credentialsRepo.upsert({
userId,
credentialId: base64url(credentialIDBuffer),
publicKey: base64url(publicKeyBuffer),
counter: registrationInfo.credential?.counter ?? 0,
aaguid,
providerLabel, // ← 认证时可直接回传做 UI 展示
});注:很多平台/浏览器默认 none attestation,AAGUID 可能是全 0 或不可用;因此只能展示,不要据此放行/拒绝。(Stack Overflow)
4) 认证成功后,把 providerLabel 回给前端(方便 UI 显示“来自…”)
ts
// 上一行:校验断言通过后,用 rawId 定位凭据
const cred = await credentialsRepo.findById(payload.rawId);
// 上一行:把之前存的 providerLabel 返回给前端,仅作展示
return {
ok: true,
providerLabel: cred?.providerLabel ?? "Unknown",
// 也可以带上 credentialDeviceType/credentialBackedUp 做补充标签
};5) 关键注意
- Apple / Safari:经常拿到全 0 AAGUID(或拿不到),这是隐私设计 + none attestation 的结果,不能唯一指向“Apple”。(Apple Developer)
- Windows Hello:存在多个 AAGUID(Software / Hardware / VBS Hardware),上面的三枚是公开常见值。(corbado.com)
- Google Password Manager:目前常见 AAGUID 为
ea9b8d66-...(历史上有过变化)。(谷歌集团) - YubiKey:型号 + 固件版本不同会有不同 AAGUID,请以官方表为准(我在样例里只挑了几条常用)。(Yubico Support)
- 用途边界:AAGUID 适合给用户看(“这把凭据来自 … ”),安全策略应以 FIDO MDS、证书链、政策(UV/UP、signCount 等)为主。(W3C)
参考
- Google Password Manager AAGUID(官方/社区多处一致)。(corbado.com)
- Windows Hello 常见 AAGUID。(corbado.com)
- YubiKey 官方 AAGUID 列表(持续更新)。(Yubico Support)
- Apple/iCloud Keychain 常见全 0 AAGUID(none attestation)。(Apple Developer)
- web.dev:用 AAGUID 仅做展示用途的最佳实践与社区清单。(web.dev)
需要的话我可以再给一版带图标的映射(顺手接入 webauthn.passwordless.id 的图标 CDN,命名为 {aaguid}-light.png / {aaguid}-dark.png),这样前端账号设置页能直接显示小图标。(webauthn.passwordless.id)