You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WebAuthn服务端(Go)与客户端(JS)挑战验证失败求助

WebAuthn注册验证失败:JS编解码函数修复方案

问题核心

服务端基于Go + go-webauthn/webauthn库,纯JS客户端实现WebAuthn设备注册流程,点击按钮触发注册后,触摸Yubikey传感器完成操作,但服务端CreateCredential()抛出验证失败错误。怀疑问题出在JS的buffer_encode和buffer_decode函数,且不理解buffer_encode中replace()语句的作用。

问题分析

1. buffer_encode中replace()的作用

btoa()生成的是标准Base64编码,但WebAuthn规范要求使用Base64 URL安全编码(避免URL转义、兼容JSON传输),三个replace()的作用就是做格式转换:

  • 把+替换为-
  • 把/替换为_
  • 去掉末尾的=填充字符
    移除这些替换后,服务端无法解析非标准Base64格式的数据,导致验证失败。

2. 当前buffer_decode的错误

服务端返回的challenge和user.id是Base64 URL编码的字符串,而你当前的buffer_decode直接将字符串的每个字符转成Unicode编码值,完全不符合Base64解码逻辑,导致传给浏览器的challenge和user.id数据错误,这是验证失败的核心原因。

正确的编解码函数

// 将ArrayBuffer转为Base64 URL安全编码(符合WebAuthn规范)
const buffer_encode = (arrayBuffer) => {
  return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=/g, "");
};

// 将Base64 URL编码字符串转为ArrayBuffer
const buffer_decode = (base64UrlStr) => {
  // 先将Base64 URL转成标准Base64格式
  let base64Str = base64UrlStr.replace(/-/g, "+").replace(/_/g, "/");
  // 补充缺失的填充字符=
  while (base64Str.length % 4 !== 0) {
    base64Str += "=";
  }
  // 解码成二进制字符串,再转成Uint8Array
  const binaryStr = atob(base64Str);
  return Uint8Array.from(binaryStr, c => c.charCodeAt(0));
};

修正后的完整JS代码

const register_device = async (event) => {
  event.preventDefault();

  // 修正后的编解码函数
  const buffer_encode = (arrayBuffer) => {
    return btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)))
      .replace(/\+/g, "-")
      .replace(/\//g, "_")
      .replace(/=/g, "");
  };

  const buffer_decode = (base64UrlStr) => {
    let base64Str = base64UrlStr.replace(/-/g, "+").replace(/_/g, "/");
    while (base64Str.length % 4 !== 0) {
      base64Str += "=";
    }
    const binaryStr = atob(base64Str);
    return Uint8Array.from(binaryStr, c => c.charCodeAt(0));
  };

  if (!window.PublicKeyCredential) {
    alert("Error: 该浏览器不支持WebAuthn");
    return;
  }

  try {
    // 发起注册初始化请求
    const beginResponse = await fetch("/2fa/v1/webauthn/register/begin");
    const credentialCreationOptions = await beginResponse.json();

    // 解码服务端返回的challenge和user.id
    credentialCreationOptions.publicKey.challenge = buffer_decode(
      credentialCreationOptions.publicKey.challenge
    );
    credentialCreationOptions.publicKey.user.id = buffer_decode(
      credentialCreationOptions.publicKey.user.id
    );

    // 调用WebAuthn API创建凭证
    const credential = await navigator.credentials.create({
      publicKey: credentialCreationOptions.publicKey
    });

    // 编码凭证数据,发送给服务端完成注册
    const finishResponse = await fetch("/2fa/v1/webauthn/register/finish", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        id: credential.id,
        rawId: buffer_encode(credential.rawId),
        type: credential.type,
        response: {
          attestationObject: buffer_encode(credential.response.attestationObject),
          clientDataJSON: buffer_encode(credential.response.clientDataJSON),
        }
      })
    });

    const result = await finishResponse.json();
    console.log(`注册结果: 状态码=${finishResponse.status} 消息=${JSON.stringify(result)}`);
  } catch (error) {
    console.error("注册过程中发生错误:", error);
  }
};

device.addEventListener("click", register_device);

额外验证建议

  1. 查看服务端具体错误信息:在Go代码中打印CreateCredential()抛出的错误详情,比如是challenge不匹配、数据格式错误还是签名验证失败,这能更精准定位问题。
  2. 检查sessionData有效性:确保注册初始化时生成的sessionData被正确存储(比如存在session或Redis中),且在finish接口中能正确获取到,因为CreateCredential()依赖该数据做验证。

内容的提问来源于stack exchange,提问作者Christian Rößner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 07:00:14