各位观众,早上好!(或者下午好、晚上好,取决于你们熬夜编程的热情程度…)
今天咱们来聊聊一个听起来高大上,但实际上贼好用的东西:WebAuthn,江湖人称“FIDO2”,它是如何革了密码的命,并让钓鱼攻击无处遁形的。
密码,你的时代要结束了!
密码这玩意,简直是安全界的“猪队友”。咱们每天要记住一大堆密码,稍不留神就被黑客盗了去。而且,密码复用、弱密码等问题层出不穷,简直防不胜防。更别提那些无聊的“找回密码”流程,简直比连续剧还长。
WebAuthn的出现,就是为了解决这些问题。它提供了一种更安全、更便捷的身份验证方式,让你彻底摆脱密码的困扰。
WebAuthn:身份验证的超级英雄
WebAuthn,全称Web Authentication,它是一种Web API,允许网站使用公钥密码学进行用户身份验证。简单来说,就是用你的设备(比如指纹识别器、面部识别器、安全密钥)来生成一对密钥:公钥和私钥。公钥交给网站,私钥留在你的设备上。每次你需要登录时,你的设备会用私钥对服务器发来的请求进行签名,然后把签名发回服务器。服务器用之前保存的公钥验证签名,如果验证成功,就说明你是本人。
这样一来,黑客就算盗走了你的公钥,也没用,因为他们没有你的私钥,无法冒充你登录。而且,WebAuthn还具有抗钓鱼攻击的能力,因为私钥只在你的设备上生成和使用,不会被泄露到互联网上。
FIDO2:WebAuthn的幕后推手
FIDO2是WebAuthn背后的技术标准,它定义了WebAuthn的工作方式和安全性要求。FIDO2由两个主要组件组成:
- CTAP(Client to Authenticator Protocol): 用于浏览器(或应用程序)和身份验证器(比如指纹识别器、安全密钥)之间的通信。
- WebAuthn API: 用于Web应用程序与浏览器之间的通信,从而触发身份验证流程。
WebAuthn的优势:
优点 | 描述 |
---|---|
安全性高 | 使用公钥密码学,私钥永远不离开你的设备,有效防止密码泄露和重放攻击。 |
抗钓鱼 | 验证过程绑定到特定的网站域名,即使黑客伪造了网站,你的身份验证器也不会上当。 |
用户体验好 | 使用指纹、面部识别等生物特征进行身份验证,无需记住复杂的密码。 |
跨平台兼容 | WebAuthn是一个开放标准,可以在各种浏览器和设备上使用。 |
无密码认证 | 允许完全摆脱密码,仅使用身份验证器进行登录。 |
多因素认证 (MFA) | 可以作为一种强大的多因素认证方式,提高账户安全性。 |
WebAuthn实战:代码撸起来!
好了,理论讲得差不多了,咱们开始撸代码。下面我将演示如何使用JavaScript实现WebAuthn的注册和登录流程。
1. 注册流程:
首先,我们需要在服务器端生成注册请求。这个请求包含一些元数据,比如网站的域名、用户的ID等。
// 服务器端代码 (Node.js)
const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const { generateRegistrationOptions } = require('@simplewebauthn/server');
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.post('/register/begin', async (req, res) => {
const { username } = req.body;
// 模拟数据库查询,检查用户名是否已存在
const user = {
id: Buffer.from(username).toString('hex'), // 将用户名转换为唯一的id,实际场景应该从数据库获取
name: username,
registered: false, // 假设用户还未注册
};
if (user.registered) {
return res.status(400).json({ error: 'User already registered' });
}
const options = await generateRegistrationOptions({
rpName: 'My Awesome App', // 网站名称
rpID: 'localhost', // 网站域名 (实际部署时需要改为真实域名)
userID: user.id, // 用户ID
userName: user.name, // 用户名
userDisplayName: user.name, // 用户显示名称
attestationType: 'direct', // attestation类型,可以设置为'direct', 'indirect', 'none'
excludeCredentials: [], // 排除已注册的凭据,避免重复注册
authenticatorSelection: {
residentKey: 'preferred', // 要求认证器存储凭据(resident key)
userVerification: 'preferred', // 要求用户验证(user verification)
},
});
// 模拟将注册选项存储到数据库中,以便稍后验证
// 在实际场景中,应该使用数据库存储
options.username = username; // 存储用户名,方便后续步骤使用
global.registrationOptions = options;
res.json(options);
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
这段代码使用 @simplewebauthn/server
库来生成注册选项。你需要替换 rpName
和 rpID
为你自己的网站信息。 userID
应该是一个唯一的标识符,用于标识用户。
接下来,我们需要在客户端使用WebAuthn API来调用身份验证器,并获取注册结果。
// 客户端代码 (JavaScript)
async function register() {
const username = document.getElementById('username').value;
const registrationData = await fetch('http://localhost:3000/register/begin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username }),
}).then(res => res.json());
if (registrationData.error) {
alert(registrationData.error);
return;
}
try {
const credential = await navigator.credentials.create({
publicKey: registrationData,
});
// 将注册结果发送到服务器进行验证
const verificationResult = await fetch('http://localhost:3000/register/finish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: registrationData.username, // 传递用户名
credential: {
id: credential.id,
rawId: Array.from(new Uint8Array(credential.rawId)), // 将 ArrayBuffer 转换为数字数组
type: credential.type,
response: {
attestationObject: Array.from(new Uint8Array(credential.response.attestationObject)), // 将 ArrayBuffer 转换为数字数组
clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)), // 将 ArrayBuffer 转换为数字数组
},
},
}),
}).then(res => res.json());
if (verificationResult.success) {
alert('Registration successful!');
} else {
alert('Registration failed: ' + verificationResult.message);
}
} catch (error) {
console.error('Registration error:', error);
alert('Registration error: ' + error.message);
}
}
这段代码首先从服务器获取注册选项,然后调用 navigator.credentials.create()
方法来创建凭据。这个方法会提示用户使用他们的身份验证器(比如指纹识别器、安全密钥)进行身份验证。
最后,我们需要将注册结果发送到服务器进行验证。
// 服务器端代码 (Node.js)
const { verifyRegistrationResponse } = require('@simplewebauthn/server');
app.post('/register/finish', async (req, res) => {
const { username, credential } = req.body;
// 模拟从数据库中获取注册选项
const options = global.registrationOptions;
const expectedChallenge = options.challenge;
try {
const verification = await verifyRegistrationResponse({
credential,
expectedChallenge,
expectedOrigin: `http://localhost:3000`, // 网站 origin (实际部署时需要改为真实 origin)
expectedRPID: 'localhost', // 网站域名 (实际部署时需要改为真实域名)
});
const { verified, registrationInfo } = verification;
if (verified) {
// 模拟将用户信息和凭据信息存储到数据库中
// 在实际场景中,应该使用数据库存储
const user = {
id: Buffer.from(username).toString('hex'),
name: username,
registered: true,
credentialId: credential.id,
credentialPublicKey: registrationInfo.credentialPublicKey,
};
global.user = user; // 模拟存储用户
return res.json({ success: true });
} else {
return res.json({ success: false, message: 'Registration verification failed' });
}
} catch (error) {
console.error('Registration verification error:', error);
return res.status(500).json({ success: false, message: error.message });
}
});
这段代码使用 @simplewebauthn/server
库来验证注册结果。你需要替换 expectedOrigin
和 expectedRPID
为你自己的网站信息。如果验证成功,你需要将用户信息和凭据信息存储到数据库中。
2. 登录流程:
登录流程与注册流程类似,首先我们需要在服务器端生成登录请求。
// 服务器端代码 (Node.js)
const { generateAuthenticationOptions } = require('@simplewebauthn/server');
app.post('/login/begin', async (req, res) => {
const { username } = req.body;
// 模拟从数据库中查询用户信息
// 在实际场景中,应该使用数据库查询
const user = global.user; // 假设在注册流程中存储了用户信息
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const options = await generateAuthenticationOptions({
allowCredentials: [{
id: user.credentialId, // 允许的凭据 ID
type: 'public-key',
}],
userVerification: 'required', // 要求用户验证(user verification)
});
// 模拟将登录选项存储到数据库中,以便稍后验证
options.username = username;
global.authenticationOptions = options;
res.json(options);
});
这段代码使用 @simplewebauthn/server
库来生成登录选项。你需要从数据库中查询用户信息,并将用户的凭据ID添加到 allowCredentials
数组中。
接下来,我们需要在客户端使用WebAuthn API来调用身份验证器,并获取登录结果。
// 客户端代码 (JavaScript)
async function login() {
const username = document.getElementById('username').value;
const authenticationData = await fetch('http://localhost:3000/login/begin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username }),
}).then(res => res.json());
if (authenticationData.error) {
alert(authenticationData.error);
return;
}
try {
const credential = await navigator.credentials.get({
publicKey: authenticationData,
});
// 将登录结果发送到服务器进行验证
const verificationResult = await fetch('http://localhost:3000/login/finish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: authenticationData.username,
credential: {
id: credential.id,
rawId: Array.from(new Uint8Array(credential.rawId)),
type: credential.type,
response: {
authenticatorData: Array.from(new Uint8Array(credential.response.authenticatorData)),
clientDataJSON: Array.from(new Uint8Array(credential.response.clientDataJSON)),
signature: Array.from(new Uint8Array(credential.response.signature)),
userHandle: credential.response.userHandle ? Array.from(new Uint8Array(credential.response.userHandle)) : null, // 用户句柄可能为空
},
},
}),
}).then(res => res.json());
if (verificationResult.success) {
alert('Login successful!');
} else {
alert('Login failed: ' + verificationResult.message);
}
} catch (error) {
console.error('Login error:', error);
alert('Login error: ' + error.message);
}
}
这段代码首先从服务器获取登录选项,然后调用 navigator.credentials.get()
方法来获取凭据。这个方法会提示用户使用他们的身份验证器进行身份验证。
最后,我们需要将登录结果发送到服务器进行验证。
// 服务器端代码 (Node.js)
const { verifyAuthenticationResponse } = require('@simplewebauthn/server');
app.post('/login/finish', async (req, res) => {
const { username, credential } = req.body;
// 模拟从数据库中获取用户信息
const user = global.user;
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const options = global.authenticationOptions;
try {
const verification = await verifyAuthenticationResponse({
credential,
expectedChallenge: options.challenge,
expectedOrigin: `http://localhost:3000`, // 网站 origin (实际部署时需要改为真实 origin)
expectedRPID: 'localhost', // 网站域名 (实际部署时需要改为真实域名)
authenticator: {
credentialID: user.credentialId,
credentialPublicKey: user.credentialPublicKey,
counter: 0, // 认证器计数器,用于防止重放攻击,实际场景中应该从数据库获取并更新
},
});
const { verified } = verification;
if (verified) {
// 登录成功
return res.json({ success: true });
} else {
return res.json({ success: false, message: 'Login verification failed' });
}
} catch (error) {
console.error('Login verification error:', error);
return res.status(500).json({ success: false, message: error.message });
}
});
这段代码使用 @simplewebauthn/server
库来验证登录结果。你需要从数据库中查询用户信息,并将其中的凭据信息传递给 verifyAuthenticationResponse
方法。如果验证成功,说明用户已经成功登录。
注意事项:
- 错误处理: 在实际开发中,你需要添加更完善的错误处理机制,以应对各种可能出现的问题。
- 数据库存储: 你需要使用数据库来存储用户信息、凭据信息和注册/登录选项。
- 安全性: 你需要采取额外的安全措施,比如防止跨站请求伪造(CSRF)攻击。
- 用户体验: 你需要设计良好的用户界面,让用户能够轻松地进行注册和登录。
- Origin 和 RPID: 务必正确配置
expectedOrigin
和expectedRPID
, 否则会导致验证失败。expectedOrigin
是网站的完整 URL (例如https://example.com
),expectedRPID
是网站的域名 (例如example.com
)。 在开发环境中,可以使用http://localhost:<port>
作为 origin,localhost
作为 RPID。 在生产环境中,务必替换为真实值。 - ArrayBuffer 转换: WebAuthn API 返回的数据通常是
ArrayBuffer
类型,需要将其转换为数字数组 (例如Array.from(new Uint8Array(arrayBuffer))
) 才能通过 JSON 序列化传递到服务器。 在服务器端,收到数据后,可能需要将其转换回Buffer
或ArrayBuffer
类型进行处理。 - Counter (计数器):
authenticator.counter
用于防止重放攻击。 每次成功认证后,认证器的计数器都会递增。 在服务器端,需要存储认证器的计数器值,并在下次认证时验证计数器值是否大于上次的值。 如果计数器值小于或等于上次的值,则说明可能存在重放攻击。
总结:
WebAuthn (FIDO2) 是一种革命性的身份验证技术,它能够有效地提高网站的安全性,并改善用户体验。 虽然实现起来可能需要一些时间和精力,但它绝对值得你投入。
希望今天的讲座能帮助你更好地理解WebAuthn。 记住,安全无小事,让我们一起努力,打造更安全的互联网!
有什么问题,欢迎提问! (我尽力回答,毕竟我也是在不断学习中…)