各位靓仔靓女们,早上好/下午好/晚上好!(取决于你们什么时候看这篇文章啦~)我是你们的老朋友,今天咱们来聊聊一个很酷炫的技术——JavaScript
的Credential Management API
,以及它在实现无密码登录中的神奇作用。
准备好你的咖啡或者茶,咱们开始今天的旅程!
一、密码,你的名字叫麻烦
在咱们深入Credential Management API
之前,先来吐槽一下密码这玩意儿。
- 用户角度: 记不住!记不住!就是记不住!要么是忘记密码,要么是设置了一堆复杂的密码,最后自己也忘了。然后就是无尽的重置密码流程,简直让人崩溃。
- 开发者角度: 存储!加密!安全!各种安全漏洞让你提心吊胆。万一数据库被黑,用户密码泄露,那就等着挨骂吧。
所以,大家都想摆脱密码这个大麻烦。那么,怎么才能在保证安全的前提下,让用户摆脱记住密码的痛苦呢?Credential Management API
就是来拯救大家的!
二、Credential Management API
:无密码登录的利器
Credential Management API
是一组JavaScript
接口,它允许网站以编程方式访问用户的凭据(credentials),并进行管理。这里的凭据,不仅仅指密码,还可以是公钥/私钥对、生物识别信息等等。
简单来说,它就像一个中间人,帮你的网站和用户的凭据存储器(比如浏览器的密码管理器、操作系统的密钥链等)打交道。这样,你的网站就不需要直接存储用户的密码,而是可以安全地使用用户已经存储的凭据来进行身份验证。
三、Credential Management API
的核心概念
Credential Management API
主要涉及到以下几个核心概念:
Credential
: 凭据,表示用户的身份验证信息。Credential
是一个抽象类,它有几个子类:PasswordCredential
:最常见的凭据类型,包含用户名和密码。PublicKeyCredential
:基于公钥加密的凭据,用于WebAuthn等更安全的身份验证方式。FederatedCredential
:用于第三方身份验证,例如使用Google、Facebook等账号登录。
CredentialsContainer
: 凭据容器,代表用户的凭据存储器。通过navigator.credentials
可以访问CredentialsContainer
对象。navigator.credentials
: 全局对象,提供访问CredentialsContainer
的入口。
四、无密码登录的工作流
咱们来一步步看看如何使用Credential Management API
实现无密码登录。
1. 注册阶段(用户首次访问网站)
-
a. 网站提供注册选项。
-
b. 用户填写注册信息(比如用户名、邮箱)。
-
c. 网站调用
navigator.credentials.create()
方法,创建一个新的PublicKeyCredential
。async function register(username, displayName) { try { const attestationOptions = await fetch('/api/register/options', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, displayName: displayName }) }).then(res => res.json()); console.log("Attestation Options:", attestationOptions); const credential = await navigator.credentials.create({ publicKey: attestationOptions }); if (!credential) { console.error("Credential creation failed."); return null; } console.log("Credential created:", credential); const verificationResponse = await fetch('/api/register/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential: { id: credential.id, rawId: arrayBufferToString(credential.rawId), type: credential.type, response: { attestationObject: arrayBufferToString(credential.response.attestationObject), clientDataJSON: arrayBufferToString(credential.response.clientDataJSON) }, }, username: username }) }).then(res => res.json()); console.log("Verification Response:", verificationResponse); if (verificationResponse.success) { console.log("Registration successful!"); return true; // Or return user object or whatever you need } else { console.error("Registration failed:", verificationResponse.message); return false; } } catch (error) { console.error("Error during registration:", error); return false; } } function arrayBufferToString(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); }
- 解释:
- 首先,从服务器获取
attestationOptions
。服务器会生成一些随机数,作为挑战(challenge),防止重放攻击。 - 然后,调用
navigator.credentials.create()
方法,传入attestationOptions
。 - 浏览器会提示用户选择一个身份验证方式(比如指纹、面部识别、硬件安全密钥)。
- 用户完成身份验证后,浏览器会生成一个
PublicKeyCredential
对象,包含公钥和一些元数据。 - 最后,将
PublicKeyCredential
发送到服务器进行验证。 - 服务器验证通过后,将用户的公钥存储起来,与用户的账户关联。
- 首先,从服务器获取
- 解释:
-
d. 服务器验证
PublicKeyCredential
,并将公钥存储起来。
2. 登录阶段(用户再次访问网站)
-
a. 网站调用
navigator.credentials.get()
方法,请求用户的凭据。async function login(username) { try { const assertionOptions = await fetch('/api/login/options', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username }) }).then(res => res.json()); console.log("Assertion Options:", assertionOptions); const credential = await navigator.credentials.get({ publicKey: assertionOptions }); if (!credential) { console.error("Credential retrieval failed."); return null; } console.log("Credential retrieved:", credential); const verificationResponse = await fetch('/api/login/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ credential: { id: credential.id, rawId: arrayBufferToString(credential.rawId), type: credential.type, response: { authenticatorData: arrayBufferToString(credential.response.authenticatorData), clientDataJSON: arrayBufferToString(credential.response.clientDataJSON), signature: arrayBufferToString(credential.response.signature), userHandle: arrayBufferToString(credential.response.userHandle) }, }, username: username }) }).then(res => res.json()); console.log("Verification Response:", verificationResponse); if (verificationResponse.success) { console.log("Login successful!"); return true; // Or return user object or whatever you need } else { console.error("Login failed:", verificationResponse.message); return false; } } catch (error) { console.error("Error during login:", error); return false; } } function arrayBufferToString(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); }
- 解释:
- 首先,从服务器获取
assertionOptions
。服务器会生成一个挑战(challenge),用于验证用户的身份。 - 然后,调用
navigator.credentials.get()
方法,传入assertionOptions
。 - 浏览器会提示用户选择一个已注册的凭据。
- 用户完成身份验证后,浏览器会返回一个
PublicKeyCredential
对象,包含签名和一些元数据。 - 最后,将
PublicKeyCredential
发送到服务器进行验证。 - 服务器使用之前存储的公钥验证签名,如果验证通过,则认为用户身份验证成功。
- 首先,从服务器获取
- 解释:
-
b. 浏览器提示用户选择一个已注册的凭据,并进行身份验证。
-
c. 网站将
PublicKeyCredential
发送到服务器进行验证。 -
d. 服务器使用之前存储的公钥验证签名,验证通过后,用户登录成功。
五、Credential Management API
的优势
- 安全性更高: 网站不需要直接存储用户的密码,避免了密码泄露的风险。
- 用户体验更好: 用户不需要记住密码,可以使用指纹、面部识别等更方便的身份验证方式。
- 跨平台兼容性:
Credential Management API
是Web标准,支持多种浏览器和操作系统。 - 支持多种身份验证方式: 除了密码,还支持公钥加密、第三方身份验证等。
六、需要注意的点
- HTTPS: 必须使用HTTPS,因为
Credential Management API
涉及到敏感信息,需要保证传输过程的安全性。 - 用户授权: 浏览器会提示用户授权,用户可以选择是否允许网站访问自己的凭据。
- 错误处理: 需要处理各种可能出现的错误,比如用户取消授权、身份验证失败等。
- 服务器端逻辑: 服务器端需要实现生成挑战(challenge)、验证凭据、存储公钥等逻辑。
七、代码示例:服务器端(Node.js + Express)
这里提供一个简单的服务器端代码示例,使用Node.js和Express框架,仅供参考。
const express = require('express');
const bodyParser = require('body-parser');
const base64url = require('base64url');
const crypto = require('crypto');
const app = express();
app.use(bodyParser.json());
// In-memory user database (for demo purposes only, use a real database in production)
const users = {};
// Helper function to generate a random challenge
function generateChallenge() {
return base64url(crypto.randomBytes(32));
}
// Register options endpoint
app.post('/api/register/options', (req, res) => {
const { username, displayName } = req.body;
// Check if username is already taken
if (users[username]) {
return res.status(400).json({ message: 'Username already exists' });
}
// Generate a challenge
const challenge = generateChallenge();
// Store the challenge in the user object (for later verification)
users[username] = {
username,
displayName,
challenge,
};
// Construct the attestation options
const attestationOptions = {
challenge: base64url.toBuffer(challenge),
rp: {
name: 'Your Website Name',
id: req.hostname, // Use your website's domain
},
user: {
id: base64url.toBuffer(username), // Use a unique user ID
name: username,
displayName: displayName,
},
pubKeyCredParams: [
{
type: 'public-key',
alg: -7, // ES256
},
{
type: 'public-key',
alg: -257, // RS256
},
],
attestation: 'none', // Or 'direct' if you want to verify the attestation statement
timeout: 60000, // 60 seconds
};
res.json(attestationOptions);
});
// Register verify endpoint
app.post('/api/register/verify', (req, res) => {
const { credential, username } = req.body;
// Retrieve the user object
const user = users[username];
if (!user) {
return res.status(400).json({ message: 'User not found' });
}
// Verify the challenge
if (credential.response.clientDataJSON.indexOf(user.challenge) === -1) {
return res.status(400).json({ message: 'Challenge mismatch' });
}
// TODO: Implement full verification logic (e.g., verify attestation statement, check origin, etc.)
// For demo purposes, we just assume the verification is successful
user.publicKey = credential.id; // Store the credential ID as the public key
res.json({ success: true, message: 'Registration successful' });
});
// Login options endpoint
app.post('/api/login/options', (req, res) => {
const { username } = req.body;
// Retrieve the user object
const user = users[username];
if (!user) {
return res.status(400).json({ message: 'User not found' });
}
// Generate a challenge
const challenge = generateChallenge();
// Store the challenge in the user object (for later verification)
user.challenge = challenge;
// Construct the assertion options
const assertionOptions = {
challenge: base64url.toBuffer(challenge),
allowCredentials: [
{
type: 'public-key',
id: base64url.toBuffer(user.publicKey), // Use the stored credential ID
},
],
userVerification: 'preferred', // Or 'required'
timeout: 60000, // 60 seconds
};
res.json(assertionOptions);
});
// Login verify endpoint
app.post('/api/login/verify', (req, res) => {
const { credential, username } = req.body;
// Retrieve the user object
const user = users[username];
if (!user) {
return res.status(400).json({ message: 'User not found' });
}
// Verify the challenge
if (credential.response.clientDataJSON.indexOf(user.challenge) === -1) {
return res.status(400).json({ message: 'Challenge mismatch' });
}
// TODO: Implement full verification logic (e.g., verify signature, check origin, etc.)
// For demo purposes, we just assume the verification is successful
res.json({ success: true, message: 'Login successful' });
});
const port = 3000;
app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
八、总结
Credential Management API
为我们提供了一种更安全、更方便的无密码登录方式。虽然实现起来稍微复杂一些,但是考虑到它带来的安全性和用户体验的提升,绝对是值得的。
希望今天的分享对你有所帮助。记住,技术是用来解决问题的,让我们一起用技术让世界变得更美好!
下次再见!