各位观众老爷们,大家好!我是今天的主讲人,咱们今天聊聊WebAuthn,一个听起来高大上,用起来倍儿安全的无密码认证技术。
开场白:密码,密码,你烦不烦?
话说,大家每天都在跟密码打交道,邮箱密码、银行密码、各种网站密码… 密码多了记不住,记住了又容易被盗。我们绞尽脑汁想出各种复杂的密码,结果还是防不住那些无孔不入的黑客。更悲催的是,辛辛苦苦设置的密码,过段时间自己都忘了!
有没有一种方法,能让我们摆脱密码的束缚,又能保证账户的安全呢?答案是肯定的!它就是我们今天的主角——WebAuthn!
WebAuthn:无密码认证的救星
WebAuthn(Web Authentication API)是一种基于公钥密码学的Web认证标准。简单来说,它允许用户使用生物识别(指纹、面容识别)或者硬件安全密钥(YubiKey、Titan Security Key等)来登录网站,而无需输入密码。
想象一下,以后登录网站,只需要轻轻一按指纹,或者插一下U盘,就能完成认证,是不是很酷炫?而且,WebAuthn的安全性比传统密码高得多,因为它利用了硬件安全模块(HSM)或者操作系统提供的安全区域来存储密钥,有效防止了密钥被盗取。
WebAuthn的工作原理:来,咱们画个图
WebAuthn认证过程涉及到三个关键角色:
- 用户 (User): 就是你,要登录网站的人。
- 注册器 (Authenticator): 你的指纹识别器、面容识别器、安全密钥等硬件设备。
- 服务器 (Server): 提供Web服务的网站。
整个认证流程可以分为两个阶段:注册 (Registration) 和认证 (Authentication)。
-
注册 (Registration):
- 用户访问网站,选择使用WebAuthn进行注册。
- 网站生成一个Challenge (挑战),发送给用户。
- 用户的浏览器调用WebAuthn API,将Challenge传递给注册器。
- 注册器生成一个密钥对(公钥和私钥),私钥保存在注册器内部的安全区域,公钥和一些认证信息(例如注册器的ID)一起返回给浏览器。
- 浏览器将公钥和认证信息发送给服务器。
- 服务器验证注册信息,并将公钥和用户账户关联起来。
-
认证 (Authentication):
- 用户访问网站,选择使用WebAuthn进行登录。
- 网站生成一个Challenge (挑战),发送给用户。
- 用户的浏览器调用WebAuthn API,将Challenge传递给注册器。
- 注册器使用私钥对Challenge进行签名,并将签名后的数据返回给浏览器。
- 浏览器将签名后的数据发送给服务器。
- 服务器使用之前保存的公钥验证签名,如果签名有效,则认证成功。
代码实战:用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}`);
});
代码解释:
- 前端代码:
- 监听
Register
和Login
按钮的点击事件。 - 调用
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 编码和解码。
- 使用
使用方法:
- 将前端代码保存为
index.html
文件。 - 将后端代码保存为
server.js
文件。 - 在终端中运行
node server.js
启动服务器。 - 在浏览器中打开
index.html
文件。 - 点击
Register
按钮进行注册,按照提示操作。 - 点击
Login
按钮进行登录,按照提示操作。
注意事项:
- 这段代码只是一个简单的示例,没有包含完整的错误处理和安全措施。在实际应用中,需要进行更严格的验证和安全防护。
- 注册器 (Authenticator) 需要支持 WebAuthn 协议。
- 浏览器需要支持 WebAuthn API。
- 后端代码中的 RP ID 需要替换成你自己的域名。
- 在生产环境中,需要使用 HTTPS 协议。
- 需要使用真正的数据库存储用户信息,而不是示例代码中的内存存储。
- 需要更严格地验证 Attestation Statement,防止恶意注册器。
- 需要考虑用户体验,例如提供友好的错误提示。
WebAuthn的优势:
- 安全性高: WebAuthn使用公钥密码学和硬件安全模块,有效防止密码被盗取和钓鱼攻击。
- 用户体验好: 无需记忆复杂的密码,使用生物识别或者硬件安全密钥即可完成认证,方便快捷。
- 跨平台: WebAuthn是Web标准,可以在各种浏览器和操作系统上使用。
- 标准化: WebAuthn是W3C和FIDO联盟共同制定的标准,具有良好的互操作性。
WebAuthn的劣势:
- 兼容性: 部分老旧浏览器可能不支持WebAuthn API。
- 硬件依赖: 需要用户拥有支持WebAuthn的注册器,例如指纹识别器、面容识别器、安全密钥等。
- 开发成本: 需要在前端和后端进行开发,需要一定的技术成本。
- 密钥丢失: 如果用户的安全密钥丢失,可能会导致账户无法访问。需要提供备用恢复机制。
WebAuthn的应用场景:
- 网站登录: 使用WebAuthn替代传统的密码登录,提高账户安全性和用户体验。
- 支付认证: 使用WebAuthn进行支付认证,防止支付欺诈。
- 企业应用: 使用WebAuthn进行企业内部应用的身份验证,提高安全性。
- 政府机构: 使用WebAuthn进行公民身份验证,提供更安全可靠的公共服务。
总结:
WebAuthn是一种非常有前景的无密码认证技术,它具有安全性高、用户体验好、跨平台、标准化等优点。虽然WebAuthn还存在一些挑战,但是随着技术的不断发展和普及,相信WebAuthn将在未来成为主流的身份验证方式。
希望今天的讲座能让大家对WebAuthn有更深入的了解。 感谢各位的观看!咱们下期再见!