
JWT鉴权机制与安全存储方案深度解析JWT鉴权的安全边界JWTJSON Web Token已经成为现代Web应用中最主流的身份认证方案之一。它的无状态特性让服务端无需维护会话信息非常适合分布式架构和微服务场景。然而JWT的安全边界在哪里当令牌被窃取时没有服务端会话可以销毁攻击者可以一直使用令牌直到过期。理解JWT的安全边界意味着我们需要在令牌的设计、传输、存储和生命周期管理四个环节都做到位。JWT核心安全机制签名算法选型算法类型密钥性能安全性适用场景HS256对称共享密钥快中单服务/内网RS256非对称公私钥对慢(验签)高微服务/开放APIES256非对称椭圆曲线快高移动端/性能敏感EdDSA非对称Ed25519极快极高新一代推荐// RS256 非对称签名示例 const crypto require(crypto); const jwt require(jsonwebtoken); // 生成公私钥对仅一次 const { publicKey, privateKey } crypto.generateKeyPairSync(rsa, { modulusLength: 2048, publicKeyEncoding: { type: spki, format: pem }, privateKeyEncoding: { type: pkcs8, format: pem } }); // 私钥签名认证服务 function issueToken(payload) { return jwt.sign(payload, privateKey, { algorithm: RS256, expiresIn: 1h, issuer: auth.example.com, jwtid: crypto.randomUUID() }); } // 公钥验签资源服务 function verifyToken(token) { return jwt.verify(token, publicKey, { algorithms: [RS256], issuer: auth.example.com }); }Token结构设计// 最小化Payload设计原则 // 不要在Payload中包含敏感信息 function createToken(user) { const payload { sub: user.id, name: user.name, role: user.role, permissions: user.permissions, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) 3600, jti: crypto.randomUUID(), iss: auth.example.com, aud: api.example.com }; return jwt.sign(payload, process.env.PRIVATE_KEY, { algorithm: RS256 }); } // 好的Payload设计 const goodPayload { sub: user_123456, name: 林蔓, role: editor, iat: 1680000000, exp: 1680003600, jti: 550e8400-e29b-41d4-a716-446655440000 }; // 错误的Payload设计包含敏感信息 const badPayload { sub: user_123456, name: 林蔓, email: linmanexample.com, ssn: 123-45-6789, internal_note: 高风险用户流水可疑, db_connection: postgres://admin:passworddb:5432/prod };Token存储安全方案分层存储策略class SecureTokenManager { constructor() { // 内存Token仅当前会话有效 this.sessionToken null; this.tokenExpiry null; } async setTokens(accessToken, refreshToken) { this.sessionToken accessToken; this.tokenExpiry this.decodeToken(accessToken).exp * 1000; // Refresh Token使用加密存储 await this.storeRefreshToken(refreshToken); } async storeRefreshToken(token) { const encrypted await this.encryptWithServerKey(token); sessionStorage.setItem(refresh_token, encrypted); } async getRefreshToken() { const encrypted sessionStorage.getItem(refresh_token); if (!encrypted) return null; try { return await this.decryptWithServerKey(encrypted); } catch { return null; } } async encryptWithServerKey(data) { const encoder new TextEncoder(); const keyData await this.getServerPublicKey(); const key await crypto.subtle.importKey( raw, keyData, { name: AES-GCM }, false, [encrypt] ); const iv crypto.getRandomValues(new Uint8Array(12)); const encrypted await crypto.subtle.encrypt( { name: AES-GCM, iv }, key, encoder.encode(data) ); const combined new Uint8Array(iv.length encrypted.byteLength); combined.set(iv); combined.set(new Uint8Array(encrypted), iv.length); return btoa(String.fromCharCode(...combined)); } async getServerPublicKey() { const response await fetch(/api/auth/public-key); const { key } await response.json(); return Uint8Array.from(atob(key), c c.charCodeAt(0)); } decodeToken(token) { try { return JSON.parse(atob(token.split(.)[1])); } catch { return null; } } getAccessToken() { if (!this.sessionToken) return null; if (Date.now() this.tokenExpiry) { return null; } return this.sessionToken; } clearTokens() { this.sessionToken null; this.tokenExpiry null; sessionStorage.removeItem(refresh_token); } }Token注入防护class XSSProtectedApp { constructor() { this.tokenManager new SecureTokenManager(); this.setupTrustedTypes(); } setupTrustedTypes() { if (window.trustedTypes window.trustedTypes.createPolicy) { window.trustedTypes.createPolicy(token-policy, { createHTML: (input) { return input.replace(/[]/g, ); }, createScriptURL: (input) { const allowed [/api/, /static/]; if (allowed.some(prefix input.startsWith(prefix))) { return input; } throw new Error(不允许的脚本URL); } }); } } sanitizeInput(input) { const div document.createElement(div); div.textContent input; return div.innerHTML; } async makeAuthenticatedRequest(url, options {}) { const token this.tokenManager.getAccessToken(); if (!token) { await this.refreshAccessToken(); } const response await fetch(url, { ...options, headers: { ...options.headers, Authorization: Bearer ${this.tokenManager.getAccessToken()} }, // Content Security Policy 头已在服务端设置 }); if (response.status 401) { await this.handleUnauthorized(); } return response; } async refreshAccessToken() { const refreshToken await this.tokenManager.getRefreshToken(); if (!refreshToken) { this.redirectToLogin(); return; } const response await fetch(/api/auth/refresh, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ refreshToken }) }); if (response.ok) { const { accessToken, refreshToken: newRefreshToken } await response.json(); await this.tokenManager.setTokens(accessToken, newRefreshToken); } else { this.redirectToLogin(); } } redirectToLogin() { this.tokenManager.clearTokens(); const returnUrl encodeURIComponent(window.location.pathname window.location.search); window.location.href /login?return_url${returnUrl}; } }Refresh Token轮换机制class RefreshTokenRotation { constructor() { this.usedTokens new Set(); this.redis null; } async init() { const Redis require(ioredis); this.redis new Redis({ host: process.env.REDIS_HOST, port: 6379, enableOfflineQueue: false }); } async issueTokens(userId, deviceInfo) { const familyId crypto.randomUUID(); const accessToken jwt.sign( { sub: userId, type: access }, process.env.JWT_SECRET, { expiresIn: 15m } ); const refreshToken jwt.sign( { sub: userId, type: refresh, family: familyId, tokenId: crypto.randomUUID() }, process.env.REFRESH_SECRET, { expiresIn: 7d } ); await this.redis.set( refresh_family:${familyId}, JSON.stringify({ userId, deviceInfo, active: true }), EX, 7 * 24 * 3600 ); return { accessToken, refreshToken }; } async rotate(refreshToken) { const decoded jwt.verify(refreshToken, process.env.REFRESH_SECRET); if (decoded.type ! refresh) { throw new Error(无效的令牌类型); } const familyKey refresh_family:${decoded.family}; const familyData await this.redis.get(familyKey); if (!familyData) { throw new Error(刷新令牌已过期); } const family JSON.parse(familyData); if (!family.active) { await this.revokeFamily(decoded.family, decoded.sub); throw new Error(令牌轮换冲突检测到安全威胁); } const usedKey used_refresh:${decoded.tokenId}; const isUsed await this.redis.setnx(usedKey, 1); if (isUsed 0) { await this.revokeFamily(decoded.family, decoded.sub); throw new Error(令牌重用检测已吊销令牌家族); } await this.redis.expire(usedKey, 7 * 24 * 3600); await this.redis.set(familyKey, JSON.stringify({ ...family, active: false }), EX, 60); return this.issueTokens(decoded.sub, family.deviceInfo); } async revokeFamily(familyId, userId) { const key refresh_family:${familyId}; const familyData await this.redis.get(key); if (familyData) { const family JSON.parse(familyData); family.active false; await this.redis.set(key, JSON.stringify(family), EX, 3600); } await this.blacklistUserTokens(userId); } async blacklistUserTokens(userId) { const blacklistKey token_blacklist:${userId}; const currentBlacklist await this.redis.get(blacklistKey) || []; const blacklist JSON.parse(currentBlacklist); const entry { timestamp: Date.now(), reason: refresh_token_reuse }; blacklist.push(entry); if (blacklist.length 100) { blacklist.shift(); } await this.redis.set(blacklistKey, JSON.stringify(blacklist), EX, 86400); } }服务端Token黑名单class TokenBlacklist { constructor() { this.cache new Map(); this.cleanupInterval setInterval(() this.cleanup(), 60000); } async blacklist(jti, expiresIn) { const entry { jti, blacklistedAt: Date.now(), expiresAt: Date.now() expiresIn }; this.cache.set(jti, entry); } async isBlacklisted(jti) { const entry this.cache.get(jti); if (!entry) return false; if (Date.now() entry.expiresAt) { this.cache.delete(jti); return false; } return true; } cleanup() { const now Date.now(); for (const [jti, entry] of this.cache) { if (now entry.expiresAt) { this.cache.delete(jti); } } } destroy() { clearInterval(this.cleanupInterval); this.cache.clear(); } }安全审计与监控class SecurityAudit { constructor() { this.events []; this.anomalyThresholds { maxLoginAttempts: 5, maxTokenRefreshes: 20, suspiciousWindow: 300000 }; } logEvent(event) { this.events.push({ ...event, timestamp: Date.now() }); if (this.events.length 10000) { this.events.shift(); } if (this.detectAnomaly(event)) { this.triggerAlert(event); } } detectAnomaly(event) { const window Date.now() - this.anomalyThresholds.suspiciousWindow; const recentEvents this.events.filter(e e.timestamp window); switch (event.type) { case login_failure: const loginFailures recentEvents.filter( e e.type login_failure e.ip event.ip ); return loginFailures.length this.anomalyThresholds.maxLoginAttempts; case token_refresh: const refreshes recentEvents.filter( e e.type token_refresh e.userId event.userId ); return refreshes.length this.anomalyThresholds.maxTokenRefreshes; case different_geo: return event.distance 1000; default: return false; } } triggerAlert(event) { console.warn(安全告警:, { type: event.type, userId: event.userId, ip: event.ip, timestamp: new Date(event.timestamp).toISOString(), detail: event.detail }); } }安全最佳实践总结安全措施实施级别说明非对称签名(RS256/ES256)高避免共享密钥泄露风险短生命周期Access Token高15-30分钟减少被盗窗口Refresh Token轮换高检测并阻止令牌重放Token绑定设备指纹中绑定User-Agent/IP范围加密存储Refresh Token中使用Web Crypto API内容安全策略(CSP)高阻止XSS攻击窃取令牌Token黑名单机制中支持服务端主动吊销异常行为检测高实时监控令牌使用模式JWT的安全不在于算法本身而在于整个认证链路的防护。从Token的签发、传输、存储到吊销每个环节都需要精心设计。合理的令牌生命周期管理结合设备指纹、异常检测等辅助手段可以在无状态认证的便利性和安全性之间找到最佳平衡点。