各位靓仔靓女,晚上好!我是你们的老朋友,今天我们来聊聊WebAuthn,这玩意儿听起来高大上,其实就是让你的网站告别密码,拥抱未来。准备好了吗? Let’s dive in!
WebAuthn: 密码已死,我来接班!
WebAuthn(Web Authentication API)是W3C的一个标准,它与FIDO2联盟的CTAP(Client to Authenticator Protocol)协议一起,组成了一套完整的无密码认证解决方案。简单来说,它让你的浏览器和你的身份验证器(比如指纹识别器、安全密钥)直接对话,不再需要用户输入密码。
核心概念:Attestation 和 Assertion
WebAuthn的核心流程可以分为两部分:
- Attestation (注册/认证器证明): 告诉网站“嘿,我是一个靠谱的认证器,我生成的密钥你可以信任!”
- Assertion (认证/密钥断言): 证明“嘿,我是这个用户,我拥有这个密钥,让我登录吧!”
这两个过程分别发生在用户注册和登录的时候。
第一幕:Attestation – 认证器自我介绍
当用户第一次在你的网站上注册时,Attestation流程就开始了。这个流程就像一个认证器向你的网站出示它的“身份证”,证明它是经过认证的,并且生成的密钥是安全的。
-
网站发起注册请求 (Registration Request):
网站首先会向浏览器发起一个注册请求,这个请求包含了各种参数,比如网站的域名(
rpId
),用户的ID(user.id
),以及认证器的偏好设置(比如是否需要用户验证,支持哪些算法等)。async function registerUser() { const options = { publicKey: { rp: { name: 'My Awesome Website', id: window.location.hostname }, user: { id: new Uint8Array(16), // 用户的唯一ID,通常是随机生成的 name: '[email protected]', displayName: 'John Doe' }, challenge: new Uint8Array(32), // 服务器生成的随机挑战值 pubKeyCredParams: [ { type: 'public-key', alg: -7 }, // ES256 { type: 'public-key', alg: -257 } // RS256 ], attestation: 'direct', // 'none', 'indirect', or 'direct' authenticatorSelection: { residentKey: 'discouraged', userVerification: 'preferred', // 'required', 'preferred', or 'discouraged' requireResidentKey: false }, timeout: 60000, // 超时时间 (毫秒) extensions: { // 可选的扩展 } } }; // 将 ArrayBufferView 转换为 Base64URL 字符串 function arrayBufferToBase64URLString(arrayBuffer) { return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))) .replace(/+/g, '-') .replace(///g, '_') .replace(/=+$/, ''); } // 将 options 中的 ArrayBufferView 转换为 Base64URL 字符串 const transformedOptions = { publicKey: { ...options.publicKey, challenge: arrayBufferToBase64URLString(options.publicKey.challenge), user: { ...options.publicKey.user, id: arrayBufferToBase64URLString(options.publicKey.user.id), } } }; // 将 transformedOptions 发送给后端,后端返回服务器生成的 challenge 和 userId const response = await fetch('/register/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(transformedOptions) }); const registrationOptions = await response.json(); // 将 Base64URL 字符串转换回 ArrayBufferView function base64URLStringToArrayBuffer(base64URLString) { const padding = '='.repeat((4 - base64URLString.length % 4) % 4); const base64 = (base64URLString + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } const finalOptions = { publicKey: { ...registrationOptions.publicKey, challenge: base64URLStringToArrayBuffer(registrationOptions.publicKey.challenge), user: { ...registrationOptions.publicKey.user, id: base64URLStringToArrayBuffer(registrationOptions.publicKey.user.id) }, pubKeyCredParams: registrationOptions.publicKey.pubKeyCredParams.map(param => ({ ...param, alg: parseInt(param.alg) // 将 alg 转换为数字 })), attestation: options.publicKey.attestation, timeout: 60000 } } // 调用 WebAuthn API let credential; try{ credential = await navigator.credentials.create(finalOptions); } catch(error){ console.error("注册失败:", error); return; } // 处理注册结果 const credentialResponse = { id: credential.id, rawId: arrayBufferToBase64URLString(credential.rawId), type: credential.type, response: { attestationObject: arrayBufferToBase64URLString(credential.response.attestationObject), clientDataJSON: arrayBufferToBase64URLString(credential.response.clientDataJSON), }, clientExtensionResults: credential.getClientExtensionResults() }; // 发送注册结果到服务器 const registrationResultResponse = await fetch('/register/finish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentialResponse) }); const registrationResult = await registrationResultResponse.json(); if (registrationResult.status === 'ok') { alert('注册成功!'); } else { alert('注册失败: ' + registrationResult.message); } }
-
浏览器向认证器发起注册请求:
浏览器接收到网站的注册请求后,会调用WebAuthn API,向认证器发起注册请求。 这个过程可能涉及到用户交互,比如要求用户触摸安全密钥,或者扫描指纹。
-
认证器生成密钥对:
认证器接收到注册请求后,会生成一个非对称密钥对:一个私钥(只有认证器知道),一个公钥。
-
认证器生成 Attestation Statement:
认证器会生成一个Attestation Statement,这个Statement包含了以下信息:
- 生成的公钥
- 认证器的信息(比如厂商、型号等)
- 一个签名,用于证明这个Statement是由认证器生成的
这个Attestation Statement就像认证器的“身份证”,证明它生成的密钥是可信的。
-
浏览器将注册结果返回给网站:
浏览器将认证器返回的注册结果(包括公钥、Attestation Statement等)发送给网站。
-
网站验证 Attestation Statement:
网站接收到注册结果后,会验证Attestation Statement,确认认证器是可信的,并且生成的密钥是安全的。 这一步通常涉及到与认证器的可信根证书进行比对。
-
网站保存用户信息和公钥:
如果Attestation Statement验证通过,网站会将用户的ID和公钥保存到数据库中。 这样,以后用户就可以使用这个公钥来进行登录了。
Attestation 的代码示例 (后端验证):
以下是一个简单的Node.js后端验证Attestation的示例代码 (简化版,实际应用中需要更严谨的验证):
const crypto = require('crypto');
const cbor = require('cbor');
const cose = require('cose-js');
async function verifyAttestation(attestationObject, clientDataJSON) {
const attestation = cbor.decodeAllSync(Buffer.from(attestationObject, 'base64'))[0];
const clientData = JSON.parse(Buffer.from(clientDataJSON, 'base64').toString());
// 验证 clientData 的 hash
const clientDataHash = crypto.createHash('sha256').update(Buffer.from(clientDataJSON, 'base64')).digest();
if (!Buffer.from(attestation.authData.slice(37)).equals(clientDataHash)) {
throw new Error('clientData hash verification failed');
}
// 验证 rpIdHash
const rpIdHash = crypto.createHash('sha256').update(Buffer.from('your_website_rp_id')).digest();
if (!Buffer.from(attestation.authData.slice(4, 36)).equals(rpIdHash)) {
throw new Error('rpIdHash verification failed');
}
// 验证 flags
const flags = attestation.authData[0];
const userPresent = flags & 0x01;
const userVerified = flags & 0x04;
const attestedCredentialDataIncluded = flags & 0x40;
if (!userPresent) {
throw new Error('User presence flag not set');
}
// 验证 attestation statement
const fmt = attestation.fmt;
const attStmt = attestation.attStmt;
if (fmt === 'packed') {
// 验证 packed attestation
const alg = attStmt.alg;
const sig = attStmt.sig;
const x5c = attStmt.x5c;
const pubArea = attestation.authData.slice(0, attestation.authData.length); // Simplified
// 这只是一个占位符,实际验证需要使用证书链进行验证
// 并且需要针对不同的 attestation format 进行不同的处理
if (!x5c || x5c.length === 0) {
throw new Error('x5c is missing');
}
return true;
} else if (fmt === 'none') {
// 无 attestation
return true;
} else {
throw new Error('Unsupported attestation format: ' + fmt);
}
}
第二幕:Assertion – 我真的是我!
当用户尝试登录时,Assertion流程就开始了。这个流程就像用户使用私钥对一个挑战值进行签名,证明他们拥有与之前注册的公钥对应的私钥。
-
网站发起登录请求 (Authentication Request):
网站首先会向浏览器发起一个登录请求,这个请求包含了各种参数,比如网站的域名(
rpId
),以及一个服务器生成的随机挑战值(challenge
)。async function loginUser() { const options = { publicKey: { challenge: new Uint8Array(32), // 服务器生成的随机挑战值 rpId: window.location.hostname, allowCredentials: [], // 允许的凭证,通常是之前注册的 credential 的 ID userVerification: 'preferred', // 'required', 'preferred', or 'discouraged' timeout: 60000, // 超时时间 (毫秒) } }; // 将 ArrayBufferView 转换为 Base64URL 字符串 function arrayBufferToBase64URLString(arrayBuffer) { return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))) .replace(/+/g, '-') .replace(///g, '_') .replace(/=+$/, ''); } // 将 options 中的 ArrayBufferView 转换为 Base64URL 字符串 const transformedOptions = { publicKey: { ...options.publicKey, challenge: arrayBufferToBase64URLString(options.publicKey.challenge), } }; const response = await fetch('/login/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(transformedOptions) }); const authenticationOptions = await response.json(); // 将 Base64URL 字符串转换回 ArrayBufferView function base64URLStringToArrayBuffer(base64URLString) { const padding = '='.repeat((4 - base64URLString.length % 4) % 4); const base64 = (base64URLString + padding) .replace(/-/g, '+') .replace(/_/g, '/'); const rawData = atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) { outputArray[i] = rawData.charCodeAt(i); } return outputArray; } const finalOptions = { publicKey: { ...authenticationOptions.publicKey, challenge: base64URLStringToArrayBuffer(authenticationOptions.publicKey.challenge), allowCredentials: authenticationOptions.publicKey.allowCredentials.map(cred => ({ ...cred, id: base64URLStringToArrayBuffer(cred.id) })), timeout: 60000 } }; // 调用 WebAuthn API let credential; try { credential = await navigator.credentials.get(finalOptions); } catch (error) { console.error("登录失败:", error); return; } // 处理登录结果 const credentialResponse = { id: credential.id, rawId: arrayBufferToBase64URLString(credential.rawId), type: credential.type, response: { authenticatorData: arrayBufferToBase64URLString(credential.response.authenticatorData), clientDataJSON: arrayBufferToBase64URLString(credential.response.clientDataJSON), signature: arrayBufferToBase64URLString(credential.response.signature), userHandle: credential.response.userHandle ? arrayBufferToBase64URLString(credential.response.userHandle) : null, }, clientExtensionResults: credential.getClientExtensionResults() }; // 发送登录结果到服务器 const authenticationResultResponse = await fetch('/login/finish', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(credentialResponse) }); const authenticationResult = await authenticationResultResponse.json(); if (authenticationResult.status === 'ok') { alert('登录成功!'); } else { alert('登录失败: ' + authenticationResult.message); } }
-
浏览器向认证器发起登录请求:
浏览器接收到网站的登录请求后,会调用WebAuthn API,向认证器发起登录请求。 这个过程同样可能涉及到用户交互。
-
认证器使用私钥对挑战值进行签名:
认证器接收到登录请求后,会使用与之前注册的公钥对应的私钥,对网站提供的挑战值进行签名。
-
浏览器将登录结果返回给网站:
浏览器将认证器返回的签名和其他相关信息(比如Authenticator Data)发送给网站。
-
网站验证签名:
网站接收到登录结果后,会使用之前保存的公钥,验证认证器返回的签名。
-
网站验证 Authenticator Data:
网站还需要验证Authenticator Data,确保用户存在,并且已经通过验证(比如指纹识别)。
-
网站允许用户登录:
如果签名和Authenticator Data验证通过,网站就认为用户是可信的,允许用户登录。
Assertion 的代码示例 (后端验证):
以下是一个简单的Node.js后端验证Assertion的示例代码 (简化版,实际应用中需要更严谨的验证):
const crypto = require('crypto');
const cbor = require('cbor');
async function verifyAssertion(authenticatorData, clientDataJSON, signature, publicKey) {
const clientData = JSON.parse(Buffer.from(clientDataJSON, 'base64').toString());
const authenticatorDataBuffer = Buffer.from(authenticatorData, 'base64');
const signatureBuffer = Buffer.from(signature, 'base64');
// 验证 clientData 的 hash
const clientDataHash = crypto.createHash('sha256').update(Buffer.from(clientDataJSON, 'base64')).digest();
if (!Buffer.from(authenticatorDataBuffer.slice(37)).equals(clientDataHash)) {
throw new Error('clientData hash verification failed');
}
// 验证 rpIdHash
const rpIdHash = crypto.createHash('sha256').update(Buffer.from('your_website_rp_id')).digest();
if (!Buffer.from(authenticatorDataBuffer.slice(4, 36)).equals(rpIdHash)) {
throw new Error('rpIdHash verification failed');
}
// 验证 flags
const flags = authenticatorDataBuffer[0];
const userPresent = flags & 0x01;
const userVerified = flags & 0x04;
if (!userPresent) {
throw new Error('User presence flag not set');
}
// 创建验证数据
const verificationData = Buffer.concat([
authenticatorDataBuffer,
crypto.createHash('sha256').update(Buffer.from(clientDataJSON, 'base64')).digest()
]);
// 验证签名
const verifier = crypto.createVerify('sha256');
verifier.update(verificationData);
const isVerified = verifier.verify(publicKey, signatureBuffer);
if (!isVerified) {
throw new Error('Signature verification failed');
}
return true;
}
WebAuthn 的优势:
- 安全性: WebAuthn使用硬件级别的安全机制,比如安全密钥、指纹识别器等,比传统的密码认证更加安全。私钥永远不会离开认证设备。
- 易用性: 用户只需要触摸安全密钥或者扫描指纹,就可以完成认证,无需记忆和输入密码。
- 防钓鱼: WebAuthn认证过程基于域名绑定,可以有效防止钓鱼攻击。
- 跨平台: WebAuthn是W3C标准,得到了主流浏览器和操作系统的支持。
WebAuthn 的挑战:
- 认证器丢失: 如果用户丢失了认证器,就需要进行恢复流程。
- 兼容性: 虽然WebAuthn得到了广泛支持,但仍然有一些老旧的浏览器和设备不支持。
- 复杂性: WebAuthn的实现细节比较复杂,需要一定的技术积累。
总结:
WebAuthn是一项非常有前景的无密码认证技术,它可以提高网站的安全性,改善用户体验。 虽然WebAuthn的实现细节比较复杂,但是随着WebAuthn的普及,相信会有越来越多的开发者掌握这项技术。
表格总结:
流程 | 步骤 | 描述 |
---|---|---|
Attestation | 1. 网站发起注册请求 | 网站向浏览器发送注册请求,包含网站域名、用户ID、认证器偏好设置等信息。 |
2. 浏览器向认证器发起注册请求 | 浏览器调用WebAuthn API,向认证器发起注册请求。 | |
3. 认证器生成密钥对 | 认证器生成一个非对称密钥对:私钥(认证器持有)和公钥。 | |
4. 认证器生成 Attestation Statement | 认证器生成一个包含公钥、认证器信息和签名的 Attestation Statement,证明密钥的可信度。 | |
5. 浏览器将注册结果返回给网站 | 浏览器将公钥和 Attestation Statement 发送给网站。 | |
6. 网站验证 Attestation Statement | 网站验证 Attestation Statement,确认认证器可信,密钥安全。 | |
7. 网站保存用户信息和公钥 | 网站将用户信息和公钥保存到数据库。 | |
Assertion | 1. 网站发起登录请求 | 网站向浏览器发送登录请求,包含网站域名和服务器生成的随机挑战值。 |
2. 浏览器向认证器发起登录请求 | 浏览器调用WebAuthn API,向认证器发起登录请求。 | |
3. 认证器使用私钥对挑战值进行签名 | 认证器使用与注册时公钥对应的私钥,对挑战值进行签名。 | |
4. 浏览器将登录结果返回给网站 | 浏览器将签名和Authenticator Data发送给网站。 | |
5. 网站验证签名 | 网站使用注册时保存的公钥验证签名。 | |
6. 网站验证 Authenticator Data | 网站验证 Authenticator Data,确认用户存在且已通过验证。 | |
7. 网站允许用户登录 | 如果签名和Authenticator Data验证通过,允许用户登录。 |
彩蛋:
最后,记住一点:安全无小事,代码需谨慎。 祝大家早日告别密码,拥抱WebAuthn的美好未来!下课!