JS `WebAuthn` API:无密码认证与生物识别安全

各位观众老爷们,大家好!我是今天的主讲人,咱们今天聊聊WebAuthn,一个听起来高大上,用起来倍儿安全的无密码认证技术。

开场白:密码,密码,你烦不烦?

话说,大家每天都在跟密码打交道,邮箱密码、银行密码、各种网站密码… 密码多了记不住,记住了又容易被盗。我们绞尽脑汁想出各种复杂的密码,结果还是防不住那些无孔不入的黑客。更悲催的是,辛辛苦苦设置的密码,过段时间自己都忘了!

有没有一种方法,能让我们摆脱密码的束缚,又能保证账户的安全呢?答案是肯定的!它就是我们今天的主角——WebAuthn!

WebAuthn:无密码认证的救星

WebAuthn(Web Authentication API)是一种基于公钥密码学的Web认证标准。简单来说,它允许用户使用生物识别(指纹、面容识别)或者硬件安全密钥(YubiKey、Titan Security Key等)来登录网站,而无需输入密码。

想象一下,以后登录网站,只需要轻轻一按指纹,或者插一下U盘,就能完成认证,是不是很酷炫?而且,WebAuthn的安全性比传统密码高得多,因为它利用了硬件安全模块(HSM)或者操作系统提供的安全区域来存储密钥,有效防止了密钥被盗取。

WebAuthn的工作原理:来,咱们画个图

WebAuthn认证过程涉及到三个关键角色:

  1. 用户 (User): 就是你,要登录网站的人。
  2. 注册器 (Authenticator): 你的指纹识别器、面容识别器、安全密钥等硬件设备。
  3. 服务器 (Server): 提供Web服务的网站。

整个认证流程可以分为两个阶段:注册 (Registration) 和认证 (Authentication)。

  • 注册 (Registration):

    1. 用户访问网站,选择使用WebAuthn进行注册。
    2. 网站生成一个Challenge (挑战),发送给用户。
    3. 用户的浏览器调用WebAuthn API,将Challenge传递给注册器。
    4. 注册器生成一个密钥对(公钥和私钥),私钥保存在注册器内部的安全区域,公钥和一些认证信息(例如注册器的ID)一起返回给浏览器。
    5. 浏览器将公钥和认证信息发送给服务器。
    6. 服务器验证注册信息,并将公钥和用户账户关联起来。
  • 认证 (Authentication):

    1. 用户访问网站,选择使用WebAuthn进行登录。
    2. 网站生成一个Challenge (挑战),发送给用户。
    3. 用户的浏览器调用WebAuthn API,将Challenge传递给注册器。
    4. 注册器使用私钥对Challenge进行签名,并将签名后的数据返回给浏览器。
    5. 浏览器将签名后的数据发送给服务器。
    6. 服务器使用之前保存的公钥验证签名,如果签名有效,则认证成功。

代码实战:用JS玩转WebAuthn

理论讲完了,咱们来点干货,用JavaScript代码来实现一个简单的WebAuthn认证流程。

前端代码 (HTML + JavaScript)

<!DOCTYPE html>
<html>
<head>
  <title>WebAuthn Example</title>
</head>
<body>
  <h1>WebAuthn Example</h1>

  <button id="registerButton">Register</button>
  <button id="loginButton">Login</button>

  <script>
    const registerButton = document.getElementById('registerButton');
    const loginButton = document.getElementById('loginButton');

    registerButton.addEventListener('click', async () => {
      try {
        const registrationOptions = await fetch('/register/options').then(res => res.json());
        const credential = await navigator.credentials.create(registrationOptions);
        const registrationResult = await fetch('/register', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            credential: {
              id: credential.id,
              rawId: arrayBufferToBase64(credential.rawId),
              type: credential.type,
              response: {
                attestationObject: arrayBufferToBase64(credential.response.attestationObject),
                clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON)
              }
            }
          })
        }).then(res => res.json());

        console.log('Registration Result:', registrationResult);
        alert('Registration successful!');

      } catch (error) {
        console.error('Registration error:', error);
        alert('Registration failed: ' + error.message);
      }
    });

    loginButton.addEventListener('click', async () => {
      try {
        const authenticationOptions = await fetch('/login/options').then(res => res.json());
        const assertion = await navigator.credentials.get(authenticationOptions);

        const authenticationResult = await fetch('/login', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            assertion: {
              id: assertion.id,
              rawId: arrayBufferToBase64(assertion.rawId),
              type: assertion.type,
              response: {
                authenticatorData: arrayBufferToBase64(assertion.response.authenticatorData),
                clientDataJSON: arrayBufferToBase64(assertion.response.clientDataJSON),
                signature: arrayBufferToBase64(assertion.response.signature),
                userHandle: assertion.response.userHandle ? arrayBufferToBase64(assertion.response.userHandle) : null
              }
            }
          })
        }).then(res => res.json());

        console.log('Authentication Result:', authenticationResult);
        alert('Authentication successful!');

      } catch (error) {
        console.error('Authentication error:', error);
        alert('Authentication failed: ' + error.message);
      }
    });

    // Helper function to convert ArrayBuffer to Base64
    function arrayBufferToBase64(buffer) {
      let binary = '';
      const bytes = new Uint8Array(buffer);
      const len = bytes.byteLength;
      for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
      }
      return btoa(binary);
    }

  </script>
</body>
</html>

后端代码 (Node.js + Express)

首先,安装必要的依赖:

npm install express cbor base64url

然后,创建 server.js 文件:

const express = require('express');
const crypto = require('crypto');
const cbor = require('cbor');
const base64url = require('base64url');

const app = express();
const port = 3000;

app.use(express.json());

// In-memory store for user credentials (replace with a database in a real application)
const users = {};

// Helper function to generate a random challenge
function generateChallenge() {
  return crypto.randomBytes(32).toString('hex');
}

// Helper function to verify attestation statement
function verifyAttestation(attestationObject, clientDataJSON) {
  // This is a simplified example and may not cover all cases.
  // In a real-world scenario, you need to perform more thorough verification.
  try {
    const attestation = cbor.decodeAllSync(Buffer.from(attestationObject, 'base64'))[0];
    const clientData = JSON.parse(Buffer.from(clientDataJSON, 'base64').toString('utf8'));

    // Verify client data hash
    const clientDataHash = crypto.createHash('sha256').update(Buffer.from(clientDataJSON, 'base64')).digest();
    if (Buffer.compare(attestation.authData.slice(0, 32), clientDataHash) !== 0) {
      console.error('Client data hash mismatch');
      return false;
    }

    // Verify RP ID hash
    const rpIdHash = crypto.createHash('sha256').update(Buffer.from('localhost', 'utf8')).digest(); // Replace 'localhost' with your actual RP ID
    if (Buffer.compare(attestation.authData.slice(32, 64), rpIdHash) !== 0) {
      console.error('RP ID hash mismatch');
      return false;
    }

    return true;
  } catch (error) {
    console.error('Attestation verification error:', error);
    return false;
  }
}

// Helper function to verify assertion signature
function verifySignature(signature, authenticatorData, clientDataJSON, publicKey) {
    try {
        const verifier = crypto.createVerify('sha256');
        verifier.update(Buffer.concat([Buffer.from(authenticatorData, 'base64'), Buffer.from(clientDataJSON, 'base64')]));
        return verifier.verify(publicKey, Buffer.from(signature, 'base64'));
    } catch (error) {
        console.error('Signature verification error:', error);
        return false;
    }
}

// Registration Options Endpoint
app.get('/register/options', (req, res) => {
  const challenge = generateChallenge();
  const userId = crypto.randomBytes(16).toString('hex'); // Generate a unique user ID

  const options = {
    challenge: base64url.encode(challenge),
    rp: {
      name: 'WebAuthn Example', // Replace with your application name
      id: 'localhost' // Replace with your application domain
    },
    user: {
      id: base64url.encode(userId),
      name: '[email protected]', // Replace with the user's email or username
      displayName: 'Example User' // Replace with the user's display name
    },
    pubKeyCredParams: [
      {
        type: 'public-key',
        alg: -7 // ES256 algorithm
      },
      {
        type: 'public-key',
        alg: -257 // RS256 algorithm
      }
    ],
    attestation: 'direct',
    timeout: 60000,
    authenticatorSelection: {
        requireResidentKey: false,
        userVerification: 'preferred',
        residentKey: 'discouraged'
    }
  };

  // Store the challenge and user ID for later verification
  users[userId] = { challenge };

  res.json(options);
});

// Registration Endpoint
app.post('/register', (req, res) => {
  const { credential } = req.body;
  const { id, rawId, type, response } = credential;
  const { attestationObject, clientDataJSON } = response;

  const userId = base64url.decode(req.body.credential.id);

  // Verify the challenge
  if (users[userId].challenge !== JSON.parse(Buffer.from(clientDataJSON, 'base64').toString()).challenge) {
      console.error('Challenge mismatch');
      return res.status(400).json({ error: 'Challenge mismatch' });
  }

  // Verify the attestation statement
  if (!verifyAttestation(attestationObject, clientDataJSON)) {
    return res.status(400).json({ error: 'Attestation verification failed' });
  }

  // Extract public key from attestation object (simplified example)
  const attestation = cbor.decodeAllSync(Buffer.from(attestationObject, 'base64'))[0];
  const publicKey = attestation.authData.slice(65); // This offset depends on the attestation format

  // Store the credential information
  users[userId] = {
    ...users[userId],
    credentialId: id,
    publicKey: publicKey.toString('hex')
  };

  res.json({ success: true });
});

// Login Options Endpoint
app.get('/login/options', (req, res) => {
  const challenge = generateChallenge();

  const options = {
    challenge: base64url.encode(challenge),
    allowCredentials: Object.keys(users).map(userId => ({
      id: users[userId].credentialId,
      type: 'public-key',
      transports: ['usb', 'nfc', 'ble'] // Adjust transports as needed
    })),
    userVerification: 'preferred',
    timeout: 60000
  };

  // Store the challenge for later verification
  Object.keys(users).forEach(userId => {
    users[userId].challenge = challenge;
  });

  res.json(options);
});

// Login Endpoint
app.post('/login', (req, res) => {
  const { assertion } = req.body;
  const { id, rawId, type, response } = assertion;
  const { authenticatorData, clientDataJSON, signature, userHandle } = response;

  const userId = base64url.decode(assertion.id);

  // Verify the challenge
  if (users[userId].challenge !== JSON.parse(Buffer.from(clientDataJSON, 'base64').toString()).challenge) {
      console.error('Challenge mismatch');
      return res.status(400).json({ error: 'Challenge mismatch' });
  }

  // Get the user's public key
  const publicKey = Buffer.from(users[userId].publicKey, 'hex');

  // Verify the signature
  if (!verifySignature(signature, authenticatorData, clientDataJSON, publicKey)) {
      return res.status(400).json({ error: 'Signature verification failed' });
  }

  res.json({ success: true });
});

app.listen(port, () => {
  console.log(`Server listening at http://localhost:${port}`);
});

代码解释:

  • 前端代码:
    • 监听 RegisterLogin 按钮的点击事件。
    • 调用 navigator.credentials.create() 进行注册,调用 navigator.credentials.get() 进行认证。
    • 将数据以JSON格式发送给后端服务器。
    • 使用 arrayBufferToBase64() 函数将 ArrayBuffer 转换为 Base64 字符串,方便数据传输。
  • 后端代码:
    • 使用 express 框架搭建服务器。
    • generateChallenge() 函数生成随机的 Challenge。
    • /register/options 路由返回注册选项,包括 Challenge、RP ID、用户ID等信息。
    • /register 路由接收注册结果,验证 Challenge 和 Attestation Statement,并将公钥和用户账户关联起来。
    • /login/options 路由返回认证选项,包括 Challenge 和允许的 Credential ID。
    • /login 路由接收认证结果,验证 Challenge 和签名,如果验证通过,则认证成功。
    • verifyAttestation 函数验证注册时的Attestation Statement.
    • verifySignature 函数验证认证时的签名。
    • 使用 cbor 库解析 Attestation Object。
    • 使用 base64url 库进行 Base64 URL 编码和解码。

使用方法:

  1. 将前端代码保存为 index.html 文件。
  2. 将后端代码保存为 server.js 文件。
  3. 在终端中运行 node server.js 启动服务器。
  4. 在浏览器中打开 index.html 文件。
  5. 点击 Register 按钮进行注册,按照提示操作。
  6. 点击 Login 按钮进行登录,按照提示操作。

注意事项:

  • 这段代码只是一个简单的示例,没有包含完整的错误处理和安全措施。在实际应用中,需要进行更严格的验证和安全防护。
  • 注册器 (Authenticator) 需要支持 WebAuthn 协议。
  • 浏览器需要支持 WebAuthn API。
  • 后端代码中的 RP ID 需要替换成你自己的域名。
  • 在生产环境中,需要使用 HTTPS 协议。
  • 需要使用真正的数据库存储用户信息,而不是示例代码中的内存存储。
  • 需要更严格地验证 Attestation Statement,防止恶意注册器。
  • 需要考虑用户体验,例如提供友好的错误提示。

WebAuthn的优势:

  1. 安全性高: WebAuthn使用公钥密码学和硬件安全模块,有效防止密码被盗取和钓鱼攻击。
  2. 用户体验好: 无需记忆复杂的密码,使用生物识别或者硬件安全密钥即可完成认证,方便快捷。
  3. 跨平台: WebAuthn是Web标准,可以在各种浏览器和操作系统上使用。
  4. 标准化: WebAuthn是W3C和FIDO联盟共同制定的标准,具有良好的互操作性。

WebAuthn的劣势:

  1. 兼容性: 部分老旧浏览器可能不支持WebAuthn API。
  2. 硬件依赖: 需要用户拥有支持WebAuthn的注册器,例如指纹识别器、面容识别器、安全密钥等。
  3. 开发成本: 需要在前端和后端进行开发,需要一定的技术成本。
  4. 密钥丢失: 如果用户的安全密钥丢失,可能会导致账户无法访问。需要提供备用恢复机制。

WebAuthn的应用场景:

  1. 网站登录: 使用WebAuthn替代传统的密码登录,提高账户安全性和用户体验。
  2. 支付认证: 使用WebAuthn进行支付认证,防止支付欺诈。
  3. 企业应用: 使用WebAuthn进行企业内部应用的身份验证,提高安全性。
  4. 政府机构: 使用WebAuthn进行公民身份验证,提供更安全可靠的公共服务。

总结:

WebAuthn是一种非常有前景的无密码认证技术,它具有安全性高、用户体验好、跨平台、标准化等优点。虽然WebAuthn还存在一些挑战,但是随着技术的不断发展和普及,相信WebAuthn将在未来成为主流的身份验证方式。

希望今天的讲座能让大家对WebAuthn有更深入的了解。 感谢各位的观看!咱们下期再见!

发表回复

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