JS `Web Authentication API` (`WebAuthn`) `FIDO2` 实现强认证

嘿,大家好!今天咱们来聊聊一个既酷炫又安全的玩意儿——WebAuthn! 别被这一堆字母唬住,其实它就是让你的网站登录像刷指纹解锁手机一样简单又安全。

WebAuthn:告别弱密码,拥抱未来

想象一下,你再也不用记住那些复杂的密码了,也不用担心密码被盗,甚至不用再收到那些烦人的验证码短信。是不是很美好?WebAuthn 就能帮你实现这个梦想。它让你的浏览器和硬件安全设备(比如指纹识别器、USB Key)配合,实现真正意义上的“强认证”。

FIDO2:WebAuthn 的好基友

WebAuthn 并不是单打独斗,它和 FIDO2 是好基友,一起构建了一个强大的认证体系。FIDO2 包含两个部分:

  • CTAP(Client to Authenticator Protocol): 浏览器通过 CTAP 和你的硬件安全设备“对话”,比如告诉它“嘿,用户想登录,你验证一下指纹”。
  • WebAuthn(Web Authentication API): 浏览器提供给 Web 开发者的 JavaScript API,让我们可以方便地在网站上集成 WebAuthn 认证。

WebAuthn 的工作原理:简单三步走

WebAuthn 的核心流程可以概括为三个步骤:注册、认证和验证。

  1. 注册(Registration):

    • 用户首次访问支持 WebAuthn 的网站,网站会生成一个“挑战(challenge)”。
    • 网站通过 WebAuthn API 将挑战发送给浏览器。
    • 浏览器再通过 CTAP 将挑战发送给硬件安全设备。
    • 硬件安全设备(比如你的指纹识别器)验证用户身份,生成一个密钥对,并将公钥返回给浏览器。
    • 浏览器将公钥和一些元数据(比如设备类型)发送给网站。
    • 网站保存这些信息,就完成了注册。
  2. 认证(Authentication):

    • 用户再次访问网站,网站同样会生成一个“挑战”。
    • 网站通过 WebAuthn API 将挑战发送给浏览器。
    • 浏览器通过 CTAP 将挑战发送给硬件安全设备。
    • 硬件安全设备使用私钥对挑战进行签名。
    • 浏览器将签名发送给网站。
  3. 验证(Verification):

    • 网站使用之前保存的公钥验证签名。
    • 如果签名正确,说明用户身份验证通过,允许登录。

代码实战:WebAuthn 的 JavaScript 实现

光说不练假把式,现在我们就来用 JavaScript 代码实现 WebAuthn 的注册和认证过程。

1. 注册(Registration)

async function register() {
  // 1. 生成注册选项
  const registrationOptions = {
    publicKey: {
      challenge: new Uint8Array(32), // 挑战值,服务端生成
      rp: {
        name: 'Example Corp', // Relying Party (RP) 名称,也就是你的网站
        id: window.location.hostname, // RP ID,通常是你的域名
      },
      user: {
        id: new Uint8Array(16), // 用户ID,服务端生成
        name: 'john.doe', // 用户名
        displayName: 'John Doe', // 显示名
      },
      pubKeyCredParams: [
        { type: 'public-key', alg: -7 }, // ES256
        { type: 'public-key', alg: -257 }, // RS256
      ],
      timeout: 60000, // 超时时间,单位毫秒
      attestation: 'direct', // 认证语句类型
    },
  };

  // 2. 调用 WebAuthn API 进行注册
  try {
    const credential = await navigator.credentials.create(registrationOptions);

    // 3. 将注册结果发送给服务端
    const attestationObject = new Uint8Array(credential.response.attestationObject);
    const clientDataJSON = new Uint8Array(credential.response.clientDataJSON);
    const rawId = new Uint8Array(credential.rawId);

    const registrationData = {
      id: credential.id,
      rawId: Array.from(rawId),
      type: credential.type,
      attestationObject: Array.from(attestationObject),
      clientDataJSON: Array.from(clientDataJSON),
    };

    // 发送 registrationData 到服务端进行验证和存储
    console.log('Registration Data:', registrationData);
    // 这里你需要使用 fetch 或其他方式将数据发送到后端
  } catch (error) {
    console.error('Registration failed:', error);
  }
}

// Helper function to convert string to ArrayBuffer
function str2ab(str) {
  const buf = new ArrayBuffer(str.length);
  const bufView = new Uint8Array(buf);
  for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
  }
  return buf;
}

代码解释:

  • registrationOptions:这是一个配置对象,告诉浏览器和安全设备注册的具体要求。
    • challenge:服务端生成的随机数,防止重放攻击。
    • rp:Relying Party,也就是你的网站的信息。
    • user:用户信息。
    • pubKeyCredParams:支持的公钥算法。
    • attestation:认证语句的类型,direct 表示直接认证。
  • navigator.credentials.create(registrationOptions):调用 WebAuthn API 进行注册,返回一个 credential 对象。
  • attestationObjectclientDataJSONrawId:这些是注册的结果,需要发送给服务端进行验证和存储。

2. 认证(Authentication)

async function authenticate() {
  // 1. 生成认证选项
  const authenticationOptions = {
    publicKey: {
      challenge: new Uint8Array(32), // 挑战值,服务端生成
      allowCredentials: [
        {
          id: '用户注册时保存的 credential ID', // credential ID,服务端获取
          type: 'public-key',
          transports: ['usb', 'nfc', 'ble'], // 可选的传输协议
        },
      ],
      timeout: 60000, // 超时时间,单位毫秒
      userVerification: 'preferred', // 用户验证方式
    },
  };

  // 2. 调用 WebAuthn API 进行认证
  try {
    const assertion = await navigator.credentials.get(authenticationOptions);

    // 3. 将认证结果发送给服务端
    const authenticatorData = new Uint8Array(assertion.response.authenticatorData);
    const clientDataJSON = new Uint8Array(assertion.response.clientDataJSON);
    const signature = new Uint8Array(assertion.response.signature);
    const userHandle = assertion.response.userHandle ? new Uint8Array(assertion.response.userHandle) : null;
    const credentialId = assertion.id;
    const authenticationData = {
      id: credentialId,
      rawId: Array.from(new Uint8Array(assertion.rawId)),
      type: assertion.type,
      authenticatorData: Array.from(authenticatorData),
      clientDataJSON: Array.from(clientDataJSON),
      signature: Array.from(signature),
      userHandle: userHandle ? Array.from(userHandle) : null,
    };

    // 发送 authenticationData 到服务端进行验证
    console.log('Authentication Data:', authenticationData);
    // 这里你需要使用 fetch 或其他方式将数据发送到后端
  } catch (error) {
    console.error('Authentication failed:', error);
  }
}

代码解释:

  • authenticationOptions:同样是一个配置对象,告诉浏览器和安全设备认证的具体要求。
    • challenge:服务端生成的随机数,防止重放攻击。
    • allowCredentials:允许使用的 credential 列表,需要提供 credential ID。
    • userVerification:用户验证方式,preferred 表示优先使用用户验证。
  • navigator.credentials.get(authenticationOptions):调用 WebAuthn API 进行认证,返回一个 assertion 对象。
  • authenticatorDataclientDataJSONsignatureuserHandle:这些是认证的结果,需要发送给服务端进行验证。

服务端验证:安全的关键

客户端的代码只是冰山一角,真正的安全保障在于服务端的验证。服务端需要完成以下几个步骤:

  1. 验证 Attestation Statement(注册时): 验证硬件安全设备的合法性,确保不是伪造的设备。不同的设备有不同的 Attestation Statement 格式,需要根据设备类型进行解析和验证。

  2. 验证 Challenge: 验证客户端发送的 Challenge 是否和之前服务端生成的一致,防止重放攻击。

  3. 验证 Origin: 验证客户端发送的 Origin 是否和网站的域名一致,防止跨站攻击。

  4. 验证签名(认证时): 使用之前保存的公钥验证客户端发送的签名是否正确,确认用户身份。

代码示例 (Node.js):

因为服务端验证涉及到复杂的密码学运算和设备认证,这里只提供一个简化的示例,展示如何验证签名。

const crypto = require('crypto');

async function verifySignature(publicKey, signature, authenticatorData, clientDataJSON) {
  try {
    // 1. 拼接数据
    const signedData = Buffer.concat([Buffer.from(authenticatorData), Buffer.from(clientDataJSON)]);

    // 2. 验证签名
    const verifier = crypto.createVerify('sha256');
    verifier.update(signedData);
    const isVerified = verifier.verify(publicKey, Buffer.from(signature));

    return isVerified;
  } catch (error) {
    console.error('Signature verification failed:', error);
    return false;
  }
}

// 使用示例
async function main() {
  const publicKey = '公钥字符串'; // 从数据库中获取
  const signature = '签名字符串'; // 从客户端接收
  const authenticatorData = 'authenticatorData 字符串'; // 从客户端接收
  const clientDataJSON = 'clientDataJSON 字符串'; // 从客户端接收

  const isSignatureValid = await verifySignature(publicKey, signature, authenticatorData, clientDataJSON);

  if (isSignatureValid) {
    console.log('Signature is valid!');
    // 认证成功
  } else {
    console.log('Signature is invalid!');
    // 认证失败
  }
}

main();

WebAuthn 的优势:不仅仅是安全

  • 安全性: WebAuthn 使用硬件安全设备进行身份验证,大大提高了安全性,有效防止密码泄露和钓鱼攻击。

  • 易用性: 用户只需要刷指纹或插入 USB Key 即可登录,无需记住复杂的密码。

  • 跨平台性: WebAuthn 是一个开放标准,支持各种浏览器和操作系统。

  • 无密码: WebAuthn 可以实现完全无密码的登录体验,提高用户体验。

WebAuthn 的局限性:需要考虑的因素

  • 硬件要求: 用户需要拥有支持 WebAuthn 的硬件安全设备,比如指纹识别器或 USB Key。

  • 兼容性: 虽然 WebAuthn 得到了广泛支持,但仍然存在一些浏览器和设备兼容性问题。

  • 复杂性: WebAuthn 的实现涉及到复杂的密码学运算和设备认证,需要一定的技术积累。

常见问题解答:

问题 解答
WebAuthn 如何防止钓鱼攻击? WebAuthn 将网站的域名绑定到密钥对,只有在正确的域名下才能使用该密钥对进行认证,从而防止钓鱼攻击。
WebAuthn 如何防止重放攻击? WebAuthn 使用 Challenge 机制,服务端每次认证都会生成一个随机数作为 Challenge,客户端必须使用私钥对 Challenge 进行签名,服务端验证签名时会检查 Challenge 是否和之前生成的一致,从而防止重放攻击。
WebAuthn 如何处理设备丢失? 如果用户丢失了硬件安全设备,需要提供备用认证方式(比如备用密码或 OTP)进行身份验证,然后注销丢失的设备,并重新注册新的设备。
WebAuthn 如何与现有认证系统集成? WebAuthn 可以作为现有认证系统的一种补充,用户可以选择使用 WebAuthn 或传统密码进行登录。
WebAuthn 需要哪些服务端依赖? 服务端需要一个 WebAuthn 库来处理注册和认证过程中的密码学运算和设备认证。常用的 WebAuthn 库包括:node-webauthn (Node.js), fido2-lib (多种语言), py_webauthn (Python) 等。
如何在开发环境测试 WebAuthn? 许多浏览器提供模拟 WebAuthn 设备的工具,或者你可以使用一个真实的 FIDO2 硬件密钥。使用模拟器可以方便地测试 WebAuthn 功能,而无需购买真实的硬件设备。

总结:WebAuthn,未来可期

WebAuthn 是一项非常有前景的技术,它为我们提供了一种更安全、更便捷的身份认证方式。虽然目前还存在一些局限性,但随着技术的不断发展和普及,WebAuthn 必将成为未来身份认证的主流。

希望今天的讲座能让你对 WebAuthn 有一个更深入的了解。 掌握了这些知识,你就可以开始在自己的网站上集成 WebAuthn 认证,为用户提供更好的安全保障。 记住,安全无小事,让我们一起拥抱 WebAuthn,共建更安全的网络世界!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注