前端到底该如何安全的实现“记住密码”?

3,565 阅读5分钟

web 应用里,“记住密码”这个小小的功能,可是咱用户的贴心小棉袄啊,用起来超级方便!但话说回来,咱们得怎样做才能既让用户享受这便利,又能牢牢护住他们的数据安全呢?这可得好好琢磨一番哦!接下来,咱们就来聊聊,有哪些靠谱的方法能实现“记住密码”这个功能,而且安全性也是杠杠的!

1. 使用 localStorage

localStorage 是一种持久化存储方式,数据在浏览器关闭后仍然存在。适用于需要长期保存的数据。

示例代码

// 生成对称密钥
async function generateSymmetricKey() {
    const key = await crypto.subtle.generateKey(
        {
            name: "AES-GCM",
            length: 256,
        },
        true,
        ["encrypt", "decrypt"]
    );
    return key;
}

// 加密数据
async function encryptData(data, key) {
    const iv = crypto.getRandomValues(new Uint8Array(12));
    const encryptedData = await crypto.subtle.encrypt(
        {
            name: "AES-GCM",
            iv: iv,
        },
        key,
        new TextEncoder().encode(data)
    );
    return { iv, encryptedData };
}

// 解密数据
async function decryptData(encryptedData, key, iv) {
    const decryptedData = await crypto.subtle.decrypt(
        {
            name: "AES-GCM",
            iv: iv,
        },
        key,
        encryptedData
    );
    return new TextDecoder().decode(decryptedData);
}

// 保存用户信息
async function saveUserInfo(username, password) {
    const key = await generateSymmetricKey();
    const { iv, encryptedData } = await encryptData(password, key);
    localStorage.setItem('username', username);
    localStorage.setItem('password', JSON.stringify({ iv, encryptedData }));
    // 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfo() {
    const username = localStorage.getItem('username');
    const { iv, encryptedData } = JSON.parse(localStorage.getItem('password'));
    const key = await generateSymmetricKey(); // 这里应使用同一个密钥
    const password = await decryptData(encryptedData, key, iv);
    return { username, password };
}

// 示例:用户登录时调用
async function login(username, password, rememberMe) {
    if (rememberMe) {
        await saveUserInfo(username, password);
    }
    // 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
    const userInfo = await getUserInfo();
    if (userInfo.username && userInfo.password) {
        document.getElementById('username').value = userInfo.username;
        document.getElementById('password').value = userInfo.password;
    }
};

2. 使用 sessionStorage

sessionStorage 是一种会话级别的存储方式,数据在浏览器关闭后会被清除。适用于需要临时保存的数据。

示例代码

// 保存用户信息
async function saveUserInfoSession(username, password) {
    const key = await generateSymmetricKey();
    const { iv, encryptedData } = await encryptData(password, key);
    sessionStorage.setItem('username', username);
    sessionStorage.setItem('password', JSON.stringify({ iv, encryptedData }));
    // 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoSession() {
    const username = sessionStorage.getItem('username');
    const { iv, encryptedData } = JSON.parse(sessionStorage.getItem('password'));
    const key = await generateSymmetricKey(); // 这里应使用同一个密钥
    const password = await decryptData(encryptedData, key, iv);
    return { username, password };
}

// 示例:用户登录时调用
async function loginSession(username, password, rememberMe) {
    if (rememberMe) {
        await saveUserInfoSession(username, password);
    }
    // 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
    const userInfo = await getUserInfoSession();
    if (userInfo.username && userInfo.password) {
        document.getElementById('username').value = userInfo.username;
        document.getElementById('password').value = userInfo.password;
    }
};

3. 使用 IndexedDB

IndexedDB 是一种更为复杂和强大的存储方式,适用于需要存储大量数据的场景。

示例代码

// 打开数据库
function openDatabase() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open('UserDatabase', 1);
        request.onupgradeneeded = (event) => {
            const db = event.target.result;
            db.createObjectStore('users', { keyPath: 'username' });
        };
        request.onsuccess = (event) => {
            resolve(event.target.result);
        };
        request.onerror = (event) => {
            reject(event.target.error);
        };
    });
}

// 保存用户信息
async function saveUserInfoIndexedDB(username, password) {
    const key = await generateSymmetricKey();
    const { iv, encryptedData } = await encryptData(password, key);
    const db = await openDatabase();
    const transaction = db.transaction('users', 'readwrite');
    const store = transaction.objectStore('users');
    store.put({ username, iv, encryptedData });
    // 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoIndexedDB(username) {
    const db = await openDatabase();
    return new Promise((resolve, reject) => {
        const transaction = db.transaction('users', 'readonly');
        const store = transaction.objectStore('users');
        const request = store.get(username);
        request.onsuccess = async (event) => {
            const result = event.target.result;
            if (result) {
                const key = await generateSymmetricKey(); // 这里应使用同一个密钥
                const password = await decryptData(result.encryptedData, key, result.iv);
                resolve({ username: result.username, password });
            } else {
                resolve(null);
            }
        };
        request.onerror = (event) => {
            reject(event.target.error);
        };
    });
}

// 示例:用户登录时调用
async function loginIndexedDB(username, password, rememberMe) {
    if (rememberMe) {
        await saveUserInfoIndexedDB(username, password);
    }
    // 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
    const username = 'exampleUsername'; // 从某处获取用户名
    const userInfo = await getUserInfoIndexedDB(username);
    if (userInfo && userInfo.username && userInfo.password) {
        document.getElementById('username').value = userInfo.username;
        document.getElementById('password').value = userInfo.password;
    }
};

4. 使用 Cookie

Cookie 是一种简单的存储方式,适用于需要在客户端和服务器之间传递少量数据的场景。需要注意的是,Cookie 的安全性较低,建议结合 HTTPS 和 HttpOnly 属性使用。

示例代码

// 设置 Cookie
function setCookie(name, value, days) {
    const date = new Date();
    date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
    const expires = "expires=" + date.toUTCString();
    document.cookie = name + "=" + value + ";" + expires + ";path=/";
}

// 获取 Cookie
function getCookie(name) {
    const nameEQ = name + "=";
    const ca = document.cookie.split(';');
    for (let i = 0; i < ca.length; i++) {
        let c = ca[i];
        while (c.charAt(0) === ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
    }
    return null;
}

// 保存用户信息
async function saveUserInfoCookie(username, password) {
    const key = await generateSymmetricKey();
    const { iv, encryptedData } = await encryptData(password, key);
    setCookie('username', username, 7);
    setCookie('password', JSON.stringify({ iv, encryptedData }), 7);
    // 密钥可以存储在更安全的地方,如服务器端
}

// 获取用户信息
async function getUserInfoCookie() {
    const username = getCookie('username');
    const { iv, encryptedData } = JSON.parse(getCookie('password'));
    const key = await generateSymmetricKey(); // 这里应使用同一个密钥
    const password = await decryptData(encryptedData, key, iv);
    return { username, password };
}

// 示例:用户登录时调用
async function loginCookie(username, password, rememberMe) {
    if (rememberMe) {
        await saveUserInfoCookie(username, password);
    }
    // 其他登录逻辑
}

// 示例:页面加载时自动填充
window.onload = async function() {
    const userInfo = await getUserInfoCookie();
    if (userInfo.username && userInfo.password) {
        document.getElementById('username').value = userInfo.username;
        document.getElementById('password').value = userInfo.password;
    }
};

5. 使用 JWT(JSON Web Token)

JWT 是一种常用的身份验证机制,特别适合在前后端分离的应用中使用。JWT 可以安全地传递用户身份信息,并且可以在客户端存储以实现“记住密码”功能。

示例代码

服务器端生成 JWT

假设你使用 Node.js 和 Express 作为服务器端框架,并使用 jsonwebtoken 库来生成和验证 JWT。

const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const SECRET_KEY = 'your_secret_key';

// 用户登录接口
app.post('/login', (req, res) => {
    const { username, password } = req.body;
    // 验证用户名和密码
    if (username === 'user' && password === 'password') {
        // 生成 JWT
        const token = jwt.sign({ username }, SECRET_KEY, { expiresIn: '1h' });
        res.json({ token });
    } else {
        res.status(401).json({ message: 'Invalid credentials' });
    }
});

// 受保护的资源
app.get('/protected', (req, res) => {
    const token = req.headers['authorization'];
    if (!token) {
        return res.status(401).json({ message: 'No token provided' });
    }
    jwt.verify(token, SECRET_KEY, (err, decoded) => {
        if (err) {
            return res.status(401).json({ message: 'Failed to authenticate token' });
        }
        res.json({ message: 'Protected resource', user: decoded.username });
    });
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

客户端存储和使用 JWT

在客户端,可以使用 localStoragesessionStorage 来存储 JWT,并在后续请求中使用。

// 用户登录
async function login(username, password, rememberMe) {
    const response = await fetch('/login', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({ username, password })
    });
    const data = await response.json();
    if (response.ok) {
        const token = data.token;
        if (rememberMe) {
            localStorage.setItem('token', token);
        } else {
            sessionStorage.setItem('token', token);
        }
    } else {
        console.error(data.message);
    }
}

// 获取受保护的资源
async function getProtectedResource() {
    const token = localStorage.getItem('token') || sessionStorage.getItem('token');
    if (!token) {
        console.error('No token found');
        return;
    }
    const response = await fetch('/protected', {
        method: 'GET',
        headers: {
            'Authorization': token
        }
    });
    const data = await response.json();
    if (response.ok) {
        console.log(data);
    } else {
        console.error(data.message);
    }
}

// 示例:用户登录时调用
document.getElementById('loginButton').addEventListener('click', async () => {
    const username = document.getElementById('username').value;
    const password = document.getElementById('password').value;
    const rememberMe = document.getElementById('rememberMe').checked;
    await login(username, password, rememberMe);
});

// 示例:页面加载时自动填充
window.onload = async function() {
    await getProtectedResource();
};

总结

如上示例,展示了如何使用 localStoragesessionStorage、IndexedDB、Cookie 和 JWT 来实现“记住密码”功能。每种方式都有其适用场景和安全考虑,大家可以根据具体需求选择合适的实现方式。

欢迎在评论区留言讨论~ Happy coding! 🚀