敏感信息根本不该存入 localstorage,加密只是补救而非解决方案;必须用 httponly cookie 管理 token 和密码,pii 数据应脱敏或按需加载,仅低风险数据可经 web crypto aes-gcm 加密存储。

直接把敏感信息加密后塞进 localStorage,不是“安全了”,而是“看起来安全了”。真正的问题不在“怎么加”,而在“该不该存”——localStorage 本身没有访问隔离、无过期机制、无法防 XSS,它天生就不该碰敏感数据。
核心结论:敏感信息根本不该进 localStorage,加密只是补救手段,不是解决方案。
✅ 优先原则:哪些数据坚决不存
原始密码、JWT token、API 密钥
→ 必须由后端通过HttpOnly + Secure + SameSite=StrictCookie 管理,前端 JavaScript 完全不可见。手机号、身份证号、银行卡号、地址等 PII(个人身份信息)
→ 前端不该持有完整值。服务端返回时就应脱敏(如138****1234),或按需加载、不缓存。长期有效的访问令牌
→ 必须设短有效期(如 15–30 分钟),配合后端黑名单/刷新机制;前端只存短期会话标识(如sessionId),且不持久化。调试开关、灰度标识、内部 API 地址
→ 应在构建时注入环境变量,或由后端动态下发,避免前端硬编码或缓存。
⚠️ 如果真要加密存(仅限低风险、可重建、非敏感数据)
比如用户主题偏好、搜索关键词、折叠面板状态等——这些可以加密,但必须满足三要素:
用 Web Crypto API + AES-GCM(别用 crypto-js)
// 正确示例(简化版)
async function encryptData(data, key, iv) {
const encoder = new TextEncoder();
const encoded = encoder.encode(data);
const cipher = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoded
);
return cipher;
}
async function decryptData(encrypted, key, iv) {
const decoder = new TextDecoder();
const decrypted = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
encrypted
);
return decoder.decode(decrypted);
}
关键要求:
- 密钥必须是
256-bit(32 字节),用importKey()显式声明length: 256 - IV 每次随机生成,严格 12 字节(GCM 推荐值),绝不复用、绝不写死
- 必须检查环境支持:
if (!window.crypto || !crypto.subtle) throw new Error('Web Crypto not available') - 加密后不能直接存
ArrayBuffer,要结构化为 JSON:
{
iv: base64url(iv),
data: base64url(cipher),
salt: base64url(salt) // 如使用 PBKDF2 派生密钥
}
✅ 推荐用
base64url(替换+→-、/→_、去掉=),比btoa()更健壮。
密钥不能落地,只能动态生成或口令派生
临时会话密钥(推荐):登录后调用
generateKey('AES-GCM', true, ['encrypt','decrypt']),密钥全程只存在内存变量中,页面关闭即销毁。
deep-java-review下载Java项目代码review工具。分析Git变更+完整调用链路上下文,推断业务需求,进行多维度评分和分类汇总,生成完整PRD文档。包含细粒度Java代码审查清单(Null安全、异常处理、Streams、并发、equals/hashCode、资源管理、API设计、性能、MyBatis/ORM、事务边界、SQL/DD...
用户口令派生(需交互):用
PBKDF2+ 随机salt+ ≥100,000 次迭代生成密钥,salt可和密文一起存,但主口令只在解密时由用户输入,绝不缓存。❌ 绝对禁止:硬编码密钥、从接口明文返回密钥、用
localStorage存密钥字符串、用Math.random()生成 IV。
?️ 白名单管控:只允许特定键名读写
localStorage 没权限系统,靠代码自律:
const WHITELIST = Object.freeze({
themeMode: true,
lastSearch: true,
sidebarCollapsed: true
});
function safeSet(key, value) {
if (!WHITELIST[key]) return;
try {
localStorage.setItem(key, JSON.stringify({
v: value,
t: Date.now(),
ver: 'v2'
}));
} catch (e) {
console.warn('缓存写入失败', e);
}
}
function safeGet(key, defaultValue = null) {
if (!WHITELIST[key]) return defaultValue;
const str = localStorage.getItem(key);
if (!str) return defaultValue;
try {
const parsed = JSON.parse(str);
// 可选:检查过期(如 7 天)
if (Date.now() - parsed.t > 7 * 24 * 60 * 60 * 1000) {
localStorage.removeItem(key);
return defaultValue;
}
return parsed.v;
} catch (e) {
localStorage.removeItem(key);
return defaultValue;
}
}
白名单中只放低风险、可公开、可重建的数据,比如界面状态、搜索历史、语言偏好——不是“能加密就能存”,而是“本就不该有敏感字段”。
? 替代方案(更安全的落地选择)
| 场景 | 推荐方式 | 说明 |
|---|---|---|
| 登录态、Token | HttpOnly Cookie |
前端完全不可读,XSS 无法窃取,服务端校验 |
| 临时会话数据 | sessionStorage |
关闭标签页自动销毁,适合单次操作缓存 |
| 复杂结构缓存 |
IndexedDB(配合加密) |
支持二进制、事务、容量更大,但仍需同源防护 |
| 用户偏好同步 | 后端配置中心 + 本地 fallback | 优先拉取服务端配置,降级用内存或 localStorage 存轻量备份 |
不复杂但容易忽略:安全边界在服务端,不在浏览器里。把敏感数据交给 localStorage,就像把保险柜钥匙贴在门上——加密只是给钥匙套了个塑料壳,而真正的锁,得装在服务器上。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










