JavaScript内核与高级编程之:`JavaScript`的`WebAuthn`:其在无密码认证中的工作原理。

各位亲爱的攻城狮、程序媛、以及未来的科技大佬们,

很高兴能有机会和大家聊聊一个既酷炫又实用的技术——WebAuthn,这玩意儿可是无密码认证领域的一把利剑。今天,咱们就来扒一扒 WebAuthn 的底裤,看看它到底是怎么实现无密码登录的,以及在 JavaScript 的世界里,我们该如何驾驭这头猛兽。

一、密码的那些糟心事儿

在正式进入 WebAuthn 的世界之前,咱们先来吐槽一下密码这货。想象一下,你是不是经常遇到以下情况:

  • 密码太多记不住? 最后只能用 "123456" 或者 "password" 这种弱密码,等着被黑客叔叔光顾。
  • 密码泄露风险高? 哪个网站要是数据库被脱裤了,你的密码可能就成了公开的秘密。
  • 每次登录都要输入密码? 简直浪费生命啊!

密码这东西,用起来麻烦,安全性还差,简直就是个鸡肋。所以,我们需要一种更安全、更便捷的认证方式,而 WebAuthn 就是那个天选之子。

二、WebAuthn:无密码认证的救星

WebAuthn (Web Authentication API) 是一种基于公钥密码学的认证标准,它允许网站利用用户设备上的认证器(Authenticator)进行身份验证,而无需用户输入密码。

啥是认证器?

认证器可以理解为一种硬件或者软件,它能够安全地存储用户的密钥对,并进行加密操作。常见的认证器包括:

  • 硬件安全密钥 (Security Key): 比如 YubiKey、Google Titan Security Key 等,长得像 U 盘,插在电脑上用。
  • TPM (Trusted Platform Module): 集成在电脑主板上的安全芯片。
  • 指纹识别器、面部识别器: 智能手机、笔记本电脑上常见的生物识别设备。
  • 平台认证器 (Platform Authenticator): 由操作系统提供的认证器,比如 Windows Hello、macOS Touch ID。

WebAuthn 的核心思想:

WebAuthn 的核心思想是利用公钥密码学,将用户的身份验证信息与设备绑定。具体来说,它包含以下几个关键步骤:

  1. 注册 (Registration): 用户在网站上注册时,会生成一对密钥,一个公钥 (Public Key) 和一个私钥 (Private Key)。公钥会被发送到网站服务器保存,私钥则安全地存储在用户的认证器中,绝不离开设备。
  2. 认证 (Authentication): 当用户登录网站时,网站会向用户的浏览器发送一个挑战 (Challenge)。浏览器会将这个挑战传递给认证器,认证器使用私钥对挑战进行签名,并将签名后的数据返回给网站。网站使用之前保存的公钥验证签名的有效性,如果验证通过,就认为用户身份验证成功。

WebAuthn 的优势:

  • 安全性高: 私钥存储在硬件设备中,不容易被盗取。即使网站数据库泄露,黑客也无法利用公钥伪造用户的身份。
  • 便捷性好: 无需输入密码,只需使用指纹、面部识别或者插入安全密钥即可完成认证。
  • 防钓鱼: WebAuthn 认证过程会验证网站的域名,可以有效防止钓鱼网站冒充合法网站。

三、WebAuthn 的工作流程

为了让大家更直观地理解 WebAuthn 的工作原理,我们用一张表格来梳理一下注册和认证的流程:

步骤 角色 动作 数据
1 用户 访问网站,点击注册按钮。 用户名、邮箱等注册信息。
2 网站前端 向网站后端发送注册请求。 包含用户注册信息。
3 网站后端 生成注册挑战 (Registration Challenge),并将其返回给网站前端。这个挑战是一个随机的字节数组,用于防止重放攻击。 注册挑战 (Registration Challenge)。
4 网站前端 调用 navigator.credentials.create() 方法,将注册挑战传递给浏览器。 PublicKeyCredentialCreationOptions 对象,包含注册挑战、用户 ID、用户名、认证器参数等信息。
5 浏览器 将注册挑战传递给用户的认证器。 注册挑战 (Registration Challenge)。
6 认证器 生成密钥对 (公钥和私钥),使用私钥对注册挑战进行签名,并将签名后的数据和公钥返回给浏览器。 PublicKeyCredential 对象,包含公钥、签名、认证器附件信息等。
7 浏览器 PublicKeyCredential 对象返回给网站前端。 PublicKeyCredential 对象。
8 网站前端 PublicKeyCredential 对象发送给网站后端。 PublicKeyCredential 对象。
9 网站后端 验证签名,保存公钥和用户 ID 的对应关系。 公钥、用户 ID。
10 用户 访问网站,点击登录按钮。 用户名。
11 网站前端 向网站后端发送登录请求。 包含用户名。
12 网站后端 生成认证挑战 (Authentication Challenge),并将其返回给网站前端。 认证挑战 (Authentication Challenge)。
13 网站前端 调用 navigator.credentials.get() 方法,将认证挑战传递给浏览器。 PublicKeyCredentialRequestOptions 对象,包含认证挑战、允许的公钥列表等信息。
14 浏览器 将认证挑战传递给用户的认证器。 认证挑战 (Authentication Challenge)。
15 认证器 使用私钥对认证挑战进行签名,并将签名后的数据返回给浏览器。 PublicKeyCredential 对象,包含签名、认证器附件信息等。
16 浏览器 PublicKeyCredential 对象返回给网站前端。 PublicKeyCredential 对象。
17 网站前端 PublicKeyCredential 对象发送给网站后端。 PublicKeyCredential 对象。
18 网站后端 使用之前保存的公钥验证签名,如果验证通过,则认为用户身份验证成功。 签名、公钥。

四、JavaScript 代码实战

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

1. 注册 (Registration)

async function register() {
  // 1. 向后端请求注册选项
  const response = await fetch('/register/options', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username: 'your_username' }) // 替换成你的用户名
  });

  const options = await response.json();

  // 2. 调用 navigator.credentials.create() 方法
  const credential = await navigator.credentials.create(options);

  // 3. 将 credential 对象发送给后端
  const registerResponse = await fetch('/register/complete', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(credential)
  });

  const registerResult = await registerResponse.json();

  if (registerResult.success) {
    alert('注册成功!');
  } else {
    alert('注册失败:' + registerResult.message);
  }
}

代码解读:

  • /register/options: 这个 URL 是你后端提供的 API,用于生成注册选项 (Registration Options)。后端会生成一个注册挑战 (Registration Challenge),并将其包含在注册选项中返回给前端。
  • navigator.credentials.create(options): 这是 WebAuthn 的核心 API,它会调用用户的认证器,生成密钥对,并返回一个 PublicKeyCredential 对象。
  • /register/complete: 这个 URL 是你后端提供的 API,用于完成注册过程。前端会将 PublicKeyCredential 对象发送给后端,后端会验证签名,保存公钥和用户 ID 的对应关系。

2. 认证 (Authentication)

async function login() {
  // 1. 向后端请求认证选项
  const response = await fetch('/login/options', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username: 'your_username' }) // 替换成你的用户名
  });

  const options = await response.json();

  // 2. 调用 navigator.credentials.get() 方法
  const credential = await navigator.credentials.get(options);

  // 3. 将 credential 对象发送给后端
  const loginResponse = await fetch('/login/complete', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(credential)
  });

  const loginResult = await loginResponse.json();

  if (loginResult.success) {
    alert('登录成功!');
    // 跳转到首页或者其他页面
  } else {
    alert('登录失败:' + loginResult.message);
  }
}

代码解读:

  • /login/options: 这个 URL 是你后端提供的 API,用于生成认证选项 (Authentication Options)。后端会生成一个认证挑战 (Authentication Challenge),并将其包含在认证选项中返回给前端。
  • navigator.credentials.get(options): 这是 WebAuthn 的核心 API,它会调用用户的认证器,使用私钥对认证挑战进行签名,并返回一个 PublicKeyCredential 对象。
  • /login/complete: 这个 URL 是你后端提供的 API,用于完成认证过程。前端会将 PublicKeyCredential 对象发送给后端,后端会验证签名,如果验证通过,则认为用户身份验证成功。

五、后端代码实现 (Node.js + Express)

光有前端代码还不够,咱们还得写一些后端代码来配合。这里我们使用 Node.js 和 Express 来实现后端 API。

1. 安装依赖

npm install express body-parser cbor @simplewebauthn/server

2. 注册 API (/register/options/register/complete)

const express = require('express');
const bodyParser = require('body-parser');
const cbor = require('cbor');
const {
  generateRegistrationOptions,
  verifyRegistrationResponse
} = require('@simplewebauthn/server');

const app = express();
app.use(bodyParser.json());

// 模拟数据库
const users = {};

app.post('/register/options', async (req, res) => {
  const { username } = req.body;

  if (!username) {
    return res.status(400).json({ message: '用户名不能为空' });
  }

  const options = await generateRegistrationOptions({
    rpName: 'Your Website', // 你的网站名称
    rpID: 'localhost', // 你的网站域名 (生产环境需要使用 HTTPS)
    userID: username, // 用户 ID (最好是唯一的)
    userName: username, // 用户名
    attestationType: 'direct', //  Attestation 类型, 生产环境推荐使用 "indirect"
    excludeCredentials: users[username] ? [{
      id: Buffer.from(users[username].credentialID, 'base64'),
      type: 'public-key'
    }] : [], // 排除已经注册的凭证
  });

  // 保存 challenge 到 session 中 (生产环境需要使用更安全的方式)
  users[username] = {
    challenge: options.challenge,
  };

  res.json(options);
});

app.post('/register/complete', async (req, res) => {
  const { id, rawId, response, type } = req.body;
  const { clientDataJSON, attestationObject } = response;
  const { username, challenge } = users[req.body.id]; // 使用 credential id 作为 user id

  if (!username || !challenge) {
    return res.status(400).json({ message: '注册挑战已过期' });
  }

  try {
    const verification = await verifyRegistrationResponse({
      attestationObject: attestationObject,
      clientDataJSON: clientDataJSON,
      expectedChallenge: challenge,
      expectedOrigin: `http://localhost:3000`, // 你的网站域名 (需要和前端保持一致)
      expectedRPID: 'localhost', // 你的网站域名 (需要和前端保持一致)
    });

    const { verified, registrationInfo } = verification;

    if (verified) {
      // 保存用户信息到数据库
      users[req.body.id] = {
        credentialID: req.body.id,
        publicKey: registrationInfo.publicKey,
        counter: 0,
      };

      return res.json({ success: true });
    } else {
      return res.status(400).json({ message: '注册验证失败' });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: '服务器错误' });
  }
});

3. 认证 API (/login/options/login/complete)

const {
  generateAuthenticationOptions,
  verifyAuthenticationResponse
} = require('@simplewebauthn/server');

app.post('/login/options', async (req, res) => {
  const { username } = req.body;

  if (!username) {
    return res.status(400).json({ message: '用户名不能为空' });
  }

  if (!users[username]) {
    return res.status(400).json({ message: '用户不存在' });
  }

  const options = await generateAuthenticationOptions({
    allowCredentials: [{
      id: Buffer.from(users[username].credentialID, 'base64'),
      type: 'public-key',
      transports: ['usb', 'nfc', 'ble']
    }],
    userVerification: 'preferred', // 推荐用户验证
  });

  // 保存 challenge 到 session 中 (生产环境需要使用更安全的方式)
  users[username].challenge = options.challenge;

  res.json(options);
});

app.post('/login/complete', async (req, res) => {
  const { id, rawId, response, type } = req.body;
  const { clientDataJSON, authenticatorData, signature } = response;
  const { username, challenge, publicKey, counter } = users[req.body.id];

  if (!username || !challenge || !publicKey) {
    return res.status(400).json({ message: '认证挑战已过期' });
  }

  try {
    const verification = await verifyAuthenticationResponse({
      authenticatorData: authenticatorData,
      clientDataJSON: clientDataJSON,
      expectedChallenge: challenge,
      expectedOrigin: `http://localhost:3000`, // 你的网站域名 (需要和前端保持一致)
      expectedRPID: 'localhost', // 你的网站域名 (需要和前端保持一致)
      signature: signature,
      publicKey: Buffer.from(publicKey, 'base64'),
      counter: counter,
    });

    const { verified, authenticationInfo } = verification;

    if (verified) {
      // 更新 counter
      users[req.body.id].counter = authenticationInfo.newCounter;

      return res.json({ success: true });
    } else {
      return res.status(400).json({ message: '认证验证失败' });
    }
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: '服务器错误' });
  }
});

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

代码解读:

  • @simplewebauthn/server: 这是一个 Node.js 库,用于简化 WebAuthn 后端开发。它提供了 generateRegistrationOptionsverifyRegistrationResponsegenerateAuthenticationOptionsverifyAuthenticationResponse 等方法,可以帮助我们快速实现 WebAuthn 的注册和认证逻辑。
  • 模拟数据库: 为了简化示例,我们使用一个简单的 JavaScript 对象 users 来模拟数据库。在实际项目中,你需要使用真正的数据库来存储用户信息。
  • 域名和 Origin: 在生产环境中,你需要使用 HTTPS,并且确保前端和后端的域名和 Origin 配置正确。

六、注意事项

  • HTTPS: WebAuthn 必须在 HTTPS 环境下使用。
  • 域名: WebAuthn 认证过程会验证网站的域名,所以你需要确保你的域名配置正确。
  • 用户体验: WebAuthn 的用户体验非常重要,你需要设计友好的界面,引导用户完成注册和认证过程。
  • 错误处理: WebAuthn 的错误处理比较复杂,你需要仔细处理各种错误情况,并向用户提供有用的错误信息。
  • Attestation: Attestation 是一种机制,用于验证认证器的真实性。在生产环境中,建议使用 "indirect" 或者 "enterprise" 类型的 Attestation。
  • 存储: 安全地存储用户公钥和凭证 ID 非常重要。使用适当的加密和访问控制措施来保护这些数据。
  • 依赖库: 选择合适的 WebAuthn 客户端和服务端依赖库,可以大大简化开发工作。

七、总结

WebAuthn 是一种非常有前途的无密码认证技术,它可以提高网站的安全性,并改善用户体验。虽然 WebAuthn 的实现过程比较复杂,但是有了 JavaScript 和各种 WebAuthn 库的帮助,我们可以更容易地驾驭这头猛兽。

希望今天的分享能够帮助大家更好地理解 WebAuthn 的原理和实现方式。 祝大家编码愉快,早日实现无密码登录!

发表回复

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