JavaScript内核与高级编程之:`JavaScript` 的 `Credential Management` API:其在无密码登录中的工作流。

各位靓仔靓女们,早上好/下午好/晚上好!(取决于你们什么时候看这篇文章啦~)我是你们的老朋友,今天咱们来聊聊一个很酷炫的技术——JavaScriptCredential 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为我们提供了一种更安全、更方便的无密码登录方式。虽然实现起来稍微复杂一些,但是考虑到它带来的安全性和用户体验的提升,绝对是值得的。

希望今天的分享对你有所帮助。记住,技术是用来解决问题的,让我们一起用技术让世界变得更美好!

下次再见!

发表回复

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