在前后端分离架构中,令牌(如 JWT、OAuth2 的 Access Token/Refresh Token)是身份验证的核心载体。令牌存储位置的选择直接影响系统安全性与用户体验—— 选对了能避免 XSS/CSRF 攻击,选错则可能导致令牌泄露、身份被盗。本文将系统讲解前端 4 种主流令牌存储方案,结合代码示例、示意图与对比表格,帮你快速掌握最佳实践。

一、令牌存储的核心诉求

在介绍具体方案前,先明确令牌存储需满足的 3 个核心需求:

  1. 安全性:防止令牌被恶意脚本(XSS)窃取,或被跨站请求(CSRF)滥用;
  2. 可用性:令牌需在需要时(如接口请求)被正确读取,且能适配 “记住登录”“会话保持” 等业务场景;
  3. 合理性:避免不必要的存储开销,且符合浏览器规范(如存储容量限制)。

接下来逐一分析 4 种主流存储方案:Cookie、LocalStorage、SessionStorage、内存存储

二、4 种令牌存储方案详解

1. Cookie:浏览器原生的 “安全容器”

原理

Cookie 是浏览器存储在本地的小型文本数据,由服务器通过Set-Cookie响应头设置,前端可通过document.cookie读取(若未配置HttpOnly)。其核心特性是:

  • 可配置过期时间(expires/max-age),控制生命周期;
  • 支持HttpOnly/Secure/SameSite等安全属性,抵御攻击;
  • 每次请求会自动携带 Cookie 到服务器(需匹配domain/path)。
令牌存储代码示例

服务器设置安全 Cookie(以 Node.js 为例)

javascript

// 后端设置Refresh Token到Cookie(敏感令牌推荐此方式)
app.get("/login", (req, res) => {
  const refreshToken = "eyJ1c2VySWQiOjEsIm5hbWUiOiJhZG1pbiIsImV4cCI6MTcyNjc4NTIwMCwiaWF0IjoxNzI2MTgwNDAwfQ.7ZtY6k7xQZ8Gt8L3X7H9J2K4M1N5P6R8S0T2U4V6W8Y0"; // 生成的Refresh Token
  // 关键:配置HttpOnly(禁止JS读取)、Secure(仅HTTPS传输)、SameSite(防CSRF)
  res.cookie("refreshToken", refreshToken, {
    httpOnly: true,        // 核心安全属性:禁止前端JS获取,抵御XSS
    secure: process.env.NODE_ENV === "production", // 生产环境强制HTTPS
    sameSite: "strict",    // 防CSRF:仅同站请求携带
    maxAge: 7 * 24 * 60 * 60 * 1000, // 有效期7天
    path: "/api"           // 仅/api路径的请求携带
  });
  // 同时返回Access Token(非敏感,可前端存储)
  res.json({ accessToken: "eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE3MjYxODQwMDAsImlhdCI6MTcyNjE4MDQwMH0.5F3a7b9D1e4G6h8J0k2L4M6N8P0Q2R4S6T8U0V2W4" });
});

前端读取非 HttpOnly Cookie(仅用于非敏感场景)

javascript

// 仅当Cookie未设置HttpOnly时可读取(不推荐存敏感令牌)
function getCookie(name) {
  const cookies = document.cookie.split("; ");
  for (const cookie of cookies) {
    const [key, value] = cookie.split("=");
    if (key === name) return decodeURIComponent(value);
  }
  return null;
}

// 读取非敏感令牌(如临时标识)
const tempToken = getCookie("tempToken");
优缺点
优点缺点
支持HttpOnly,彻底抵御 XSS 攻击需配置SameSite/CSRF Token防 CSRF 攻击
自动随请求携带,无需前端手动处理存储容量小(约 4KB)
可精确控制过期时间和生效路径跨域场景下需额外配置CORS(如withCredentials

2. LocalStorage:持久化的本地存储

原理

LocalStorage 是 HTML5 新增的本地存储方案,特点是:

  • 持久化存储(除非手动删除或清除浏览器数据);
  • 存储容量大(约 5-10MB);
  • 仅前端可通过localStorageAPI 操作,不与服务器自动交互;
  • 遵循同源策略(同协议、同域名、同端口)。
令牌存储代码示例

javascript

// 1. 存储令牌(如Access Token,非敏感场景)
function setTokenToLocalStorage(key, token) {
  try {
    // 建议对敏感信息简单加密(如Base64,仅防明文泄露)
    const encryptedToken = btoa(token); // 实际项目建议用AES等强加密
    localStorage.setItem(key, encryptedToken);
  } catch (e) {
    console.error("LocalStorage存储失败(可能容量已满):", e);
  }
}

// 2. 读取令牌
function getTokenFromLocalStorage(key) {
  try {
    const encryptedToken = localStorage.getItem(key);
    if (!encryptedToken) return null;
    return atob(encryptedToken); // 解密
  } catch (e) {
    console.error("LocalStorage读取失败:", e);
    return null;
  }
}

// 3. 删除令牌(如登出时)
function removeTokenFromLocalStorage(key) {
  localStorage.removeItem(key);
}

// 实际使用
const accessToken = "eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE3MjYxODQwMDAsImlhdCI6MTcyNjE4MDQwMH0.5F3a7b9D1e4G6h8J0k2L4M6N8P0Q2R4S6T8U0V2W4";
// 存储Access Token
setTokenToLocalStorage("accessToken", accessToken);
// 读取Access Token(请求接口时)
const token = getTokenFromLocalStorage("accessToken");
// 登出时删除
removeTokenFromLocalStorage("accessToken");
优缺点
优点缺点
存储容量大,适合非敏感的大体积数据完全暴露给前端 JS,易受 XSS 攻击窃取令牌
持久化存储,适合 “记住登录” 场景不自动过期,需手动管理生命周期(如设置过期时间字段)
API 简洁,易操作不随请求自动携带,需前端手动在请求头添加(如 Authorization)

3. SessionStorage:会话级的临时存储

原理

SessionStorage 与 LocalStorageAPI 完全一致,但生命周期不同:

  • 仅在当前会话有效(关闭标签页 / 浏览器后自动清除);
  • 同一浏览器的不同标签页间不共享数据(各自独立);
  • 同样遵循同源策略,存储容量约 5-10MB。
令牌存储代码示例

javascript

// 1. 存储会话级令牌(如临时Access Token,关闭标签页失效)
function setTokenToSessionStorage(key, token) {
  try {
    sessionStorage.setItem(key, token);
  } catch (e) {
    console.error("SessionStorage存储失败:", e);
  }
}

// 2. 读取令牌(如请求接口时)
function getTokenFromSessionStorage(key) {
  return sessionStorage.getItem(key) || null;
}

// 3. 登出时清除
function clearSessionToken(key) {
  sessionStorage.removeItem(key);
}

// 实际使用(如单次会话登录)
const tempAccessToken = "eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE3MjYxODI2MDAsImlhdCI6MTcyNjE4MDQwMH0.2A4b6D8f1G3h5J7k9L1M3N5P7Q9R1S3T5U7V9W1Y3";
setTokenToSessionStorage("tempAccessToken", tempAccessToken);

// 请求接口时携带
axios.get("/api/user", {
  headers: {
    Authorization: `Bearer ${getTokenFromSessionStorage("tempAccessToken")}`
  }
});
优缺点
优点缺点
会话级生命周期,自动清除,降低泄露风险标签页关闭后失效,不支持 “记住登录”
标签页隔离,多标签页间数据不共享,更安全仍易受 XSS 攻击(同 LocalStorage)
适合临时令牌(如一次性 Access Token)需手动携带令牌到请求头

4. 内存存储:最安全的临时方案

原理

内存存储指将令牌保存在前端变量中(如 Vue 的data、React 的useState),特点是:

  • 生命周期与页面实例一致(页面刷新、路由跳转后丢失);
  • 完全不持久化,浏览器关闭或刷新后数据消失;
  • 不与任何存储介质交互,仅存在于运行时内存中。
令牌存储代码示例(以 React 为例)

javascript

import { useState, useEffect } from "react";
import axios from "axios";

function UserPage() {
  // 内存存储令牌(页面刷新后丢失)
  const [accessToken, setAccessToken] = useState(null);

  // 登录获取令牌(仅存于内存)
  const handleLogin = async (username, password) => {
    const res = await axios.post("/api/login", { username, password });
    // 仅将令牌存入内存变量
    setAccessToken(res.data.accessToken);
  };

  // 请求接口时使用内存中的令牌
  const fetchUserInfo = async () => {
    if (!accessToken) return;
    try {
      const res = await axios.get("/api/user/info", {
        headers: {
          Authorization: `Bearer ${accessToken}`
        }
      });
      console.log("用户信息:", res.data);
    } catch (e) {
      console.error("请求失败:", e);
    }
  };

  return (
    <div>
      <button onClick={() => handleLogin("admin", "123456")}>登录</button>
      <button onClick={fetchUserInfo}>获取用户信息</button>
    </div>
  );
}

export default UserPage;
优缺点
优点缺点
完全抵御 XSS 攻击(无法通过脚本读取内存变量)页面刷新 / 路由跳转后丢失,需重新获取令牌
无存储容量限制,操作最轻便不支持 “记住登录”,用户体验较差
无需配置任何安全属性仅适合极致安全且无需持久化的场景(如金融类临时操作)

三、4 种令牌存储方案对比表格

对比维度Cookie(带安全配置)LocalStorageSessionStorage内存存储
安全性高(HttpOnly 防 XSS)低(易受 XSS)中(会话级,仍防 XSS)极高(仅运行时存在)
生命周期可配置(expires)持久化(手动清除)会话级(标签页关闭失效)页面实例级(刷新失效)
存储容量约 4KB约 5-10MB约 5-10MB无限制(内存足够)
自动携带到后端是(匹配 domain/path)否(需手动添加)否(需手动添加)否(需手动添加)
跨标签页共享是(同 domain)是(同 domain)否(标签页隔离)否(实例隔离)
适用令牌类型Refresh Token(敏感)Access Token(非敏感)临时 Access Token高敏感临时令牌

四、令牌存储选择策略

  1. 敏感令牌(如 Refresh Token):优先选Cookie(带 HttpOnly/Secure/SameSite)

    • 理由:HttpOnly禁止 JS 读取,从根源抵御 XSS;SameSite+CSRF Token可防 CSRF,安全性最高。
  2. 非敏感短期令牌(如 Access Token,有效期 15-30 分钟):选SessionStorage

    • 理由:会话级生命周期,关闭标签页自动清除,降低泄露风险;标签页隔离,适合多账号登录场景。
  3. 非敏感长期令牌(如 “记住登录” 的 Access Token):选LocalStorage(需加密)

    • 理由:持久化存储,无需频繁登录;但需对令牌加密(如 AES),并加强 XSS 防护(如 CSP、输入过滤)。
  4. 极高安全需求令牌(如支付临时令牌):选内存存储

    • 理由:仅存在于当前页面实例,刷新即失,即使 XSS 也无法窃取;缺点是用户体验差,需权衡。

五、安全防护最佳实践

  1. 令牌加密:LocalStorage/SessionStorage 存储的令牌需加密(避免明文泄露),推荐用crypto-js实现 AES 加密:

    javascript

    import CryptoJS from "crypto-js";
    
    // 加密(密钥需后端同步,或从安全接口获取)
    const encryptToken = (token, secretKey) => {
      return CryptoJS.AES.encrypt(token, secretKey).toString();
    };
    
    // 解密
    const decryptToken = (encryptedToken, secretKey) => {
      const bytes = CryptoJS.AES.decrypt(encryptedToken, secretKey);
      return bytes.toString(CryptoJS.enc.Utf8);
    };
    
    // 示例:加密Access Token
    const rawToken = "eyJ1c2VySWQiOjEsInVzZXJOYW1lIjoiYWRtaW4iLCJleHAiOjE3MjYxODQwMDAsImlhdCI6MTcyNjE4MDQwMH0.5F3a7b9D1e4G6h8J0k2L4M6N8P0Q2R4S6T8U0V2W4";
    const secretKey = "your-secure-key-from-backend-123"; // 密钥需后端安全下发
    const encrypted = encryptToken(rawToken, secretKey);
    const decrypted = decryptToken(encrypted, secretKey);
    
  2. 防 XSS 攻击

    • 启用 CSP(内容安全策略),限制脚本加载源;
    • 对用户输入进行过滤(如用DOMPurify净化 HTML);
    • 敏感令牌避免存 LocalStorage/SessionStorage。
  3. 防 CSRF 攻击

    • Cookie 配置SameSite: strict/lax
    • 后端校验CSRF Token(如前端从 Cookie 读取非 HttpOnly 的 CSRF Token,添加到请求头)。
  4. 令牌过期管理

    • Access Token 设短有效期(15-30 分钟),Refresh Token 设长有效期(7-30 天);
    • 过期前自动用 Refresh Token 刷新 Access Token,避免用户重新登录。

六、总结

令牌存储无 “银弹”,需根据安全性需求、生命周期、用户体验三者权衡选择:

  • 安全优先选 Cookie(HttpOnly)或内存存储;
  • 体验优先选 LocalStorage(持久化)或 SessionStorage(会话级);
  • 核心原则:敏感令牌不暴露给前端 JS,非敏感令牌需加密 + 防 XSS,同时做好 CSRF 防护。

掌握不同存储方案的特性与适用场景,才能在实际项目中构建安全、可靠的身份验证体系。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐