如何将PublicKeyCredential发送至.NET 6 Web API服务器?
解决方案
核心问题:ArrayBuffer的序列化与反序列化
WebAuthn的PublicKeyCredential包含的ArrayBuffer类型数据无法直接JSON序列化,必须转换为Base64URL无填充格式(WebAuthn标准指定的编码方式)才能安全传输。你遇到的"Attestation Object is invalid"错误,大概率是因为用了普通Base64而非Base64URL,或者编码/解码时的填充处理错误。
前端(Angular)处理步骤
- 编写工具函数,将
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(/=+$/, ''); }
- 生成凭证后,转换为可序列化的对象并发送到服务器:
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)处理步骤
- 创建对应的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; } }
- 编写工具函数,将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); }
- 在接口中处理注册请求,调用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
相关产品推荐
相关产品推荐

