Skip to content

快速回答

是的,把当前用户绑定的所有凭据(credentialId 列表,不是“类型”)都给浏览器是更稳妥的主路线。即使本机没有该 passkey,也可以通过 跨设备(Hybrid/caBLE) 用手机(iPhone/Android)扫码登录:浏览器弹出“用附近设备/手机的通行密钥”选项,展示二维码,用户用手机确认后完成签名。苹果官方与 WebAuthn 规范均明确支持这一路径。(Apple Developer)

这意味着:在 Windows 的 Chrome 上,即使用户只在 iPhone(iCloud Keychain)里有 passkey,也能通过用 iPhone 登录扫码完成认证,前端无需硬拒绝。(苹果支持)


推荐策略(更合理的业界做法)

  1. 优先基于真实凭据,而不是猜供应商:把用户已绑定的所有 credentialId 作为 allowCredentials 发给浏览器;若用 Conditional UI(表单自动填充),规范要求把 allowCredentials 设为空数组,让浏览器列出可发现凭据。(web.dev)

  2. 启用跨设备能力:为每个 allowCredentials 描述符补上 transports,包含 'hybrid' 等,以便触发“手机扫码登录”的混合传输。规范与 MDN 都有“hybrid/获取可用传输方式”的说明。(W3C)

  3. 做能力探测与兜底

    • PublicKeyCredential.isConditionalMediationAvailable() → 支持则走 Conditional UI;(MDN Web Docs)
    • 失败(如设备上没有匹配凭据/用户取消)则回退到 allowCredentials: [] 或提示“换设备/用手机”。
  4. “类型 type”建议在验证完成后再落定:后端从本次使用的 credentialId 查到供应商(可用 AAGUID→ 供应商 映射),再记录 1=苹果/2=谷歌。这样比前置“猜类型”健壮。(Apple Developer)


仅需修改的代码片段

以下是在你已有实现基础上的最小改动: 1)更新“选择类型”的函数:Windows+Chrome 且只绑了苹果时,不再返回 -1,而是返回 1,允许跨设备; 2)补一个登录兜底:先用 allowList 试;失败则退回 空 allowList +(可用时)Conditional UI; 3)构建 allowCredentials 时带上 transports: ['hybrid', 'internal', 'usb', 'nfc', 'ble']

1)更新你之前的 decidePasskeyType(只改 Windows 分支)

ts
// 复杂逻辑:Windows 上若无谷歌但绑定了苹果,允许走“混合传输(iPhone 扫码)”
if (isWindows) {
  if (isChromium && hasGoogle) return 2;
  if (hasApple) return 1; // ← 新增:不再直接 -1,支持用 iPhone 跨设备登录
  return -1;
}

依据:WebAuthn L3 支持 hybrid 传输;苹果官方支持在非 Apple 设备上用 iPhone 扫码使用 passkey。(W3C)

2)构建 allowCredentials(新增一个小工具)

ts
// 复杂逻辑:为每个已绑定凭据提供 transports,开启跨设备选项(hybrid)
function buildAllowCredentials(creds: { id: string }[]) {
  const transports: AuthenticatorTransport[] = [
    "hybrid",
    "internal",
    "usb",
    "nfc",
    "ble",
  ];
  return creds.map((c) => ({
    type: "public-key",
    id: base64urlToUint8Array(c.id),
    transports, // ← 关键
  }));
}

规范与 MDN 对 hybrid/传输有说明;大多数现代 passkey 为“可发现凭据”。(W3C)

3)登录兜底逻辑(先 allowList,失败再空列表 + Conditional UI)

ts
// 复杂逻辑:先尝试“精确匹配已绑定凭据”;失败再回退到“让浏览器列出所有可发现凭据”
export async function loginWithFallback(opts: {
  challenge: Uint8Array;
  userCreds: { id: string }[]; // 你的数据库里拿到的本用户全部 credentialId
}) {
  // 尝试 1:用 allowCredentials 精确匹配(含 hybrid,支持跨设备)
  try {
    return await startAuthentication({
      publicKey: {
        challenge: opts.challenge,
        allowCredentials: buildAllowCredentials(opts.userCreds),
        timeout: 45000,
        userVerification: "preferred",
      },
    });
  } catch (e: any) {
    // 复杂逻辑:没有可用凭据/用户取消等 -> 回退到 Conditional UI 或空 allowList
    const supportConditional =
      await PublicKeyCredential.isConditionalMediationAvailable?.();

    return await startAuthentication({
      publicKey: {
        challenge: opts.challenge,
        // 关键:空 allowCredentials 让浏览器展示“可发现凭据”(含手机扫码)
        allowCredentials: [],
        userVerification: "preferred",
      },
      // 条件式 UI 可用则用 'conditional',否则退到默认
      mediation: supportConditional ? "conditional" : ("optional" as any),
    });
  }
}

资料:

  • Conditional UI/表单自动填充与能力探测(isConditionalMediationAvailable)。(MDN Web Docs)
  • allowCredentials → 由浏览器列出可发现凭据。(simplewebauthn.dev)

小提示:Chrome 传入多个 allowCredentials 时,UI 可能只挑一个呈现,选择策略并非由你控制,这是正常现象。(Stack Overflow)


服务器端两点备注

  • 生成认证参数:把数据库中的所有本用户凭据(按你愿意的优先级排序)传给 generateAuthenticationOptions({ allowCredentials });若走 Conditional UI 的路由,则按规范清空 allowCredentials。SimpleWebAuthn 文档与规范对这点都有说明。(simplewebauthn.dev)
  • 确定“type”:验证成功后,后端能拿到本次使用的 credentialID(SimpleWebAuthn 的返回会携带,近期版本将其规范为 base64url 字符串),再根据你存的 AAGUID→ 供应商 映射落定 1/2;不建议在认证前硬判供应商。(GitHub)

为什么这是主流最佳实践

  • 跨设备是官方一等公民:WebAuthn L3/CTAP 明确“hybrid”传输;当本机没有该 passkey 时,可用手机扫码完成登录。(W3C)
  • Discoverable credentials/Passkeys 普及:空 allowCredentials 让浏览器列出所有可发现凭据,配合 Conditional UI 达到一键直登体验。(web.dev)
  • 苹果官方明确在非 Apple 设备上也能用 iPhone 的 passkey(二维码/附近设备)。(Apple Developer)

结论

  • 你的理解是对的:把用户已绑定的所有凭据给浏览器,浏览器会自动选择本机/外接/手机可用的那一个。
  • 在 Windows Chrome 但只绑定了苹果时,不要提前判 -1。按本文最小改动:类型返回 1,前端通过 allowCredentials + hybrid兜底的空列表/Conditional UI,就能让用户用 iPhone 扫码完成登录
  • 长期看,把“类型”挪到验证后基于 credentialID→AAGUID→供应商 来落定,才是更健壮的主路线。

本站总访问