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

如何将PublicKeyCredential发送至.NET 6 Web API服务器?

解决方案

核心问题:ArrayBuffer的序列化与反序列化

WebAuthn的PublicKeyCredential包含的ArrayBuffer类型数据无法直接JSON序列化,必须转换为Base64URL无填充格式(WebAuthn标准指定的编码方式)才能安全传输。你遇到的"Attestation Object is invalid"错误,大概率是因为用了普通Base64而非Base64URL,或者编码/解码时的填充处理错误。

前端(Angular)处理步骤

  1. 编写工具函数,将ArrayBuffer转换为Base64URL字符串:
function arrayBufferToBase64Url(buffer: ArrayBuffer): string {
  const bytes = new Uint8Array(buffer);
  let str = '';
  for (const byte of bytes) {
    str += String.fromCharCode(byte);
  }
  // 转Base64后替换为URL安全格式,去掉填充
  return btoa(str)
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}
  1. 生成凭证后,转换为可序列化的对象并发送到服务器:
async function register(username: string) {
  // 1. 请求服务器获取挑战值(省略这部分,你已实现)
  const challengeResponse = await fetch('/api/auth/registration-challenge', {
    method: 'POST',
    body: JSON.stringify({ username }),
    headers: { 'Content-Type': 'application/json' }
  });
  const options = await challengeResponse.json();

  // 2. 转换挑战值为Uint8Array(服务器返回的挑战值应为Base64URL格式,需转回来)
  options.challenge = Uint8Array.from(atob(options.challenge.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
  // 服务器返回的user.id如果是字符串,也要转成Uint8Array
  options.user.id = Uint8Array.from(atob(options.user.id.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));

  // 3. 创建凭证
  const credential = await navigator.credentials.create({ publicKey: options }) as PublicKeyCredential;

  // 4. 转换凭证为可序列化对象
  const serializableCredential = {
    id: credential.id,
    type: credential.type,
    rawId: arrayBufferToBase64Url(credential.rawId),
    response: {
      attestationObject: arrayBufferToBase64Url((credential.response as AuthenticatorAttestationResponse).attestationObject),
      clientDataJSON: arrayBufferToBase64Url((credential.response as AuthenticatorAttestationResponse).clientDataJSON)
    }
  };

  // 5. 发送到服务器
  const registerResponse = await fetch('/api/auth/register', {
    method: 'POST',
    body: JSON.stringify(serializableCredential),
    headers: { 'Content-Type': 'application/json' }
  });
  const result = await registerResponse.json();
  console.log(result);
}

后端(.NET 6 Web API)处理步骤

  1. 创建对应的DTO类,接收前端发送的数据:
public class PublicKeyCredentialDto
{
    public string Id { get; set; }
    public string Type { get; set; }
    public string RawId { get; set; }
    public CredentialResponseDto Response { get; set; }
}

public class CredentialResponseDto
{
    public string AttestationObject { get; set; }
    public string ClientDataJSON { get; set; }
}
  1. 编写工具函数,将Base64URL字符串转换为byte[]:
public static byte[] Base64UrlDecode(string base64Url)
{
    // 恢复Base64格式,添加必要的填充
    base64Url = base64Url.Replace('-', '+').Replace('_', '/');
    switch (base64Url.Length % 4)
    {
        case 2: base64Url += "=="; break;
        case 3: base64Url += "="; break;
    }
    return Convert.FromBase64String(base64Url);
}
  1. 在接口中处理注册请求,调用Fido2库:
[HttpPost("register")]
public async Task<IActionResult> Register([FromBody] PublicKeyCredentialDto credentialDto)
{
    // 从Session或缓存中取出之前生成的挑战值(必须和前端使用的挑战值一致)
    var challengeOptions = HttpContext.Session.Get<Fido2.Requests.MakeCredentialRequest>("registration_challenge");
    if (challengeOptions == null)
    {
        return BadRequest("Challenge expired or not found");
    }

    // 转换前端数据为Fido2库需要的格式
    var attestationResponse = new Fido2.Requests.AttestationResponse
    {
        Id = credentialDto.Id,
        RawId = Base64UrlDecode(credentialDto.RawId),
        Type = credentialDto.Type,
        Response = new Fido2.Requests.AttestationResponse.AttestationResponseDetails
        {
            AttestationObject = Base64UrlDecode(credentialDto.Response.AttestationObject),
            ClientDataJson = Base64UrlDecode(credentialDto.Response.ClientDataJSON)
        }
    };

    try
    {
        // 验证并生成凭证
        var result = await _fido2.MakeNewCredentialAsync(attestationResponse, challengeOptions);
        
        // 存储凭证到数据库(示例:关联到对应用户的凭证列表)
        // await _userRepository.AddCredentialAsync(username, result.CredentialData);
        
        return Ok(new { Success = true, CredentialId = result.CredentialId });
    }
    catch (Exception ex)
    {
        return BadRequest($"Registration failed: {ex.Message}");
    }
}

关键注意事项

  • 挑战值一致性:服务器生成的挑战值必须临时存储(如Session、Redis),验证时取出匹配,否则会导致验证失败。
  • 编码格式:严格使用Base64URL无填充格式,普通Base64会触发Fido2库的解析错误。
  • User ID处理:服务器返回的user.id需转换为Uint8Array格式供前端使用,否则创建凭证时会失败。

分离前后端额外建议

  • 配置正确的CORS规则,允许前端域名访问API接口。
  • 若采用分布式部署,建议用Redis替代Session存储挑战值,提升扩展性。

内容的提问来源于stack exchange,提问作者RangerRick

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 10:20:31