前端令牌存储完全指南:从原理到实践(含代码示例)
在前后端分离架构中,令牌(如 JWT、OAuth2 的 Access Token/Refresh Token)是身份验证的核心载体。令牌存储位置的选择直接影响系统安全性与用户体验—— 选对了能避免 XSS/CSRF 攻击,选错则可能导致令牌泄露、身份被盗。本文将系统讲解前端 4 种主流令牌存储方案,结合代码示例、示意图与对比表格,帮你快速掌握最佳实践。
一、令牌存储的核心诉求
在介绍具体方案前,先明确令牌存储需满足的 3 个核心需求:
- 安全性:防止令牌被恶意脚本(XSS)窃取,或被跨站请求(CSRF)滥用;
- 可用性:令牌需在需要时(如接口请求)被正确读取,且能适配 “记住登录”“会话保持” 等业务场景;
- 合理性:避免不必要的存储开销,且符合浏览器规范(如存储容量限制)。
接下来逐一分析 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(带安全配置) | LocalStorage | SessionStorage | 内存存储 |
|---|---|---|---|---|
| 安全性 | 高(HttpOnly 防 XSS) | 低(易受 XSS) | 中(会话级,仍防 XSS) | 极高(仅运行时存在) |
| 生命周期 | 可配置(expires) | 持久化(手动清除) | 会话级(标签页关闭失效) | 页面实例级(刷新失效) |
| 存储容量 | 约 4KB | 约 5-10MB | 约 5-10MB | 无限制(内存足够) |
| 自动携带到后端 | 是(匹配 domain/path) | 否(需手动添加) | 否(需手动添加) | 否(需手动添加) |
| 跨标签页共享 | 是(同 domain) | 是(同 domain) | 否(标签页隔离) | 否(实例隔离) |
| 适用令牌类型 | Refresh Token(敏感) | Access Token(非敏感) | 临时 Access Token | 高敏感临时令牌 |
四、令牌存储选择策略
-
敏感令牌(如 Refresh Token):优先选Cookie(带 HttpOnly/Secure/SameSite)
- 理由:
HttpOnly禁止 JS 读取,从根源抵御 XSS;SameSite+CSRF Token可防 CSRF,安全性最高。
- 理由:
-
非敏感短期令牌(如 Access Token,有效期 15-30 分钟):选SessionStorage
- 理由:会话级生命周期,关闭标签页自动清除,降低泄露风险;标签页隔离,适合多账号登录场景。
-
非敏感长期令牌(如 “记住登录” 的 Access Token):选LocalStorage(需加密)
- 理由:持久化存储,无需频繁登录;但需对令牌加密(如 AES),并加强 XSS 防护(如 CSP、输入过滤)。
-
极高安全需求令牌(如支付临时令牌):选内存存储
- 理由:仅存在于当前页面实例,刷新即失,即使 XSS 也无法窃取;缺点是用户体验差,需权衡。
五、安全防护最佳实践
-
令牌加密: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); -
防 XSS 攻击:
- 启用 CSP(内容安全策略),限制脚本加载源;
- 对用户输入进行过滤(如用
DOMPurify净化 HTML); - 敏感令牌避免存 LocalStorage/SessionStorage。
-
防 CSRF 攻击:
- Cookie 配置
SameSite: strict/lax; - 后端校验
CSRF Token(如前端从 Cookie 读取非 HttpOnly 的 CSRF Token,添加到请求头)。
- Cookie 配置
-
令牌过期管理:
- Access Token 设短有效期(15-30 分钟),Refresh Token 设长有效期(7-30 天);
- 过期前自动用 Refresh Token 刷新 Access Token,避免用户重新登录。
六、总结
令牌存储无 “银弹”,需根据安全性需求、生命周期、用户体验三者权衡选择:
- 安全优先选 Cookie(HttpOnly)或内存存储;
- 体验优先选 LocalStorage(持久化)或 SessionStorage(会话级);
- 核心原则:敏感令牌不暴露给前端 JS,非敏感令牌需加密 + 防 XSS,同时做好 CSRF 防护。
掌握不同存储方案的特性与适用场景,才能在实际项目中构建安全、可靠的身份验证体系。
更多推荐



所有评论(0)