JWT令牌在多端跨域场景下的安全访问校验实践
JWT令牌在多端跨域场景下的安全访问校验实践跨域认证的挑战现代前端应用往往需要服务于多个端Web端、移动端H5、小程序、第三方嵌入等。不同端的域名、运行环境和存储策略各不相同如何在保证安全的前提下实现统一的身份认证是每个前端架构师都需要面对的问题。JWTJSON Web Token因其无状态、跨语言、可自包含的特点成为跨域认证的首选方案。但在实际落地中令牌的传输、存储和校验涉及诸多安全细节。JWT结构回顾组成部分内容说明Header{alg:HS256,typ:JWT}签名算法和令牌类型Payload{sub:123,name:林蔓,iat:1516239022}声明数据SignatureHMACSHA256(base64UrlEncode(header).base64UrlEncode(payload), secret)防篡改签名// JWT 生成示例服务端 const jwt require(jsonwebtoken); const token jwt.sign( { sub: user_123456, name: 林蔓, role: admin, iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) 3600 }, process.env.JWT_SECRET, { algorithm: HS256 } );多端跨域场景分析场景域名存储方式传输方式安全级别主站Webexample.comhttpOnly CookieCookie自动携带高子域名sub.example.comhttpOnly Cookiedomain设置Cookie自动携带高完全跨域other.comlocalStorageAuthorization Header中移动端App-原生安全存储Authorization Header中第三方嵌入embed.comiframe postMessageAuthorization Header低令牌传输方案对比Cookie方案同域/子域// 服务端设置httpOnly Cookie const cookieOptions { httpOnly: true, secure: true, sameSite: lax, maxAge: 3600 * 1000, path: / }; // 子域共享Cookie app.use((req, res, next) { const token generateToken(req.user); res.cookie(token, token, { ...cookieOptions, domain: .example.com }); next(); });// 前端无需手动处理Cookie请求自动携带 fetch(/api/user/profile, { credentials: include }).then(res res.json());Authorization Header方案跨域// 前端存储并发送Token class TokenManager { static async getToken() { let token localStorage.getItem(access_token); if (this.isTokenExpired(token)) { token await this.refreshToken(); } return token; } static isTokenExpired(token) { if (!token) return true; try { const payload JSON.parse(atob(token.split(.)[1])); return payload.exp * 1000 Date.now(); } catch { return true; } } static async refreshToken() { const refreshToken localStorage.getItem(refresh_token); const response await fetch(/api/auth/refresh, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${localStorage.getItem(access_token)} }, body: JSON.stringify({ refreshToken }) }); if (response.ok) { const { accessToken, refreshToken: newRefreshToken } await response.json(); localStorage.setItem(access_token, accessToken); localStorage.setItem(refresh_token, newRefreshToken); return accessToken; } this.redirectToLogin(); return null; } static redirectToLogin() { localStorage.clear(); const currentPath encodeURIComponent(window.location.pathname); window.location.href /login?redirect${currentPath}; } } // 请求拦截器自动注入Token async function apiRequest(url, options {}) { const token await TokenManager.getToken(); const response await fetch(url, { ...options, headers: { ...options.headers, Authorization: Bearer ${token} } }); if (response.status 401) { const newToken await TokenManager.refreshToken(); if (newToken) { return apiRequest(url, options); } } return response; }iframe嵌入场景postMessage// 父页面主站 const childFrame document.getElementById(embedded-app); function sendTokenToChild() { const token localStorage.getItem(access_token); childFrame.contentWindow.postMessage({ type: AUTH_TOKEN, payload: { token } }, https://child.example.com); } window.addEventListener(message, (event) { if (event.origin ! https://child.example.com) return; if (event.data.type TOKEN_RECEIVED) { console.log(子页面认证成功); } }); // 子页面嵌入应用 window.addEventListener(message, async (event) { if (event.origin ! https://parent.example.com) return; if (event.data.type AUTH_TOKEN) { const { token } event.data.payload; sessionStorage.setItem(access_token, token); event.source.postMessage({ type: TOKEN_RECEIVED }, event.origin); } });安全存储策略对比存储方式XSS防护CSRF防护持久性跨域共享httpOnly Cookie自动防护JS不可读需SameSite/CSRF Token可设置持久子域共享localStorage易受XSS攻击无需CSRF防护非自动携带持久不可跨域sessionStorage易受XSS攻击无需CSRF防护会话级不可跨域Memory变量安全安全页面刷新丢失不可IndexedDB易受XSS攻击无需CSRF防护持久不可跨域增强的Token存储方案双重令牌机制class SecureTokenStore { constructor() { this.memoryToken null; } async init() { const token await this.loadToken(); if (token) { this.memoryToken token; this.clearPersistentStore(); } } async loadToken() { const encrypted sessionStorage.getItem(encrypted_token); if (!encrypted) return null; try { const token await this.decrypt(encrypted); return token; } catch { return null; } } async saveToken(token) { this.memoryToken token; const encrypted await this.encrypt(token); sessionStorage.setItem(encrypted_token, encrypted); } async encrypt(token) { const encoder new TextEncoder(); const data encoder.encode(token); const key await this.getOrCreateKey(); const iv crypto.getRandomValues(new Uint8Array(12)); const encrypted await crypto.subtle.encrypt( { name: AES-GCM, iv }, key, 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 decrypt(encoded) { const combined Uint8Array.from(atob(encoded), c c.charCodeAt(0)); const iv combined.slice(0, 12); const data combined.slice(12); const key await this.getOrCreateKey(); const decrypted await crypto.subtle.decrypt( { name: AES-GCM, iv }, key, data ); return new TextDecoder().decode(decrypted); } async getOrCreateKey() { const existing sessionStorage.getItem(crypto_key); if (existing) { const keyData Uint8Array.from(atob(existing), c c.charCodeAt(0)); return crypto.subtle.importKey(raw, keyData, AES-GCM, false, [encrypt, decrypt]); } const key await crypto.subtle.generateKey( { name: AES-GCM, length: 256 }, true, [encrypt, decrypt] ); const exported await crypto.subtle.exportKey(raw, key); sessionStorage.setItem(crypto_key, btoa(String.fromCharCode(...new Uint8Array(exported)))); return key; } getToken() { return this.memoryToken; } clearPersistentStore() { sessionStorage.removeItem(encrypted_token); sessionStorage.removeItem(crypto_key); } clear() { this.memoryToken null; this.clearPersistentStore(); } }服务端校验中间件const jwt require(jsonwebtoken); function authMiddleware(options {}) { const { extractors [ req req.cookies?.token, req req.headers.authorization?.replace(Bearer , ), req req.headers[x-access-token] ], algorithms [HS256, RS256] } options; return async (req, res, next) { let token null; for (const extractor of extractors) { token extractor(req); if (token) break; } if (!token) { return res.status(401).json({ code: UNAUTHORIZED, message: 未提供令牌 }); } try { const decoded jwt.verify(token, process.env.JWT_SECRET, { algorithms, issuer: csdn-users, clockTolerance: 30 }); req.user decoded; if (options.allowRefresh this.shouldRefresh(decoded)) { const newToken jwt.sign( { ...decoded, iat: Math.floor(Date.now() / 1000) }, process.env.JWT_SECRET, { expiresIn: 1h } ); res.setHeader(X-New-Token, newToken); } next(); } catch (error) { if (error.name TokenExpiredError) { return res.status(401).json({ code: TOKEN_EXPIRED, message: 令牌已过期, expiredAt: error.expiredAt }); } return res.status(401).json({ code: INVALID_TOKEN, message: 无效的令牌 }); } }; }CORS配置与凭证处理const cors require(cors); const allowedOrigins [ https://www.example.com, https://sub.example.com, https://embed.example.com ]; app.use(cors({ origin: (origin, callback) { if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error(不允许的跨域来源)); } }, credentials: true, allowedHeaders: [Content-Type, Authorization, X-Refresh-Token], exposedHeaders: [X-New-Token, X-Token-Expired], maxAge: 86400 }));刷新令牌轮换机制class TokenRotationManager { constructor() { this.usedRefreshTokens new Set(); } async rotate(refreshToken, userId) { if (this.usedRefreshTokens.has(refreshToken)) { await this.revokeAllUserTokens(userId); throw new Error(令牌重用检测已吊销所有令牌); } const decoded jwt.verify(refreshToken, process.env.REFRESH_SECRET); if (decoded.sub ! userId) { throw new Error(刷新令牌用户不匹配); } this.usedRefreshTokens.add(refreshToken); const newAccessToken jwt.sign( { sub: userId }, process.env.JWT_SECRET, { expiresIn: 1h } ); const newRefreshToken jwt.sign( { sub: userId, jti: crypto.randomUUID() }, process.env.REFRESH_SECRET, { expiresIn: 7d } ); return { accessToken: newAccessToken, refreshToken: newRefreshToken }; } async revokeAllUserTokens(userId) { console.log(吊销用户 ${userId} 的所有令牌); } }多端登录状态同步const ws new WebSocket(wss://api.example.com/ws); ws.addEventListener(message, (event) { const message JSON.parse(event.data); if (message.type TOKEN_REVOKED) { TokenManager.clearTokens(); showNotification(您的账号在其他设备登录); setTimeout(() { window.location.href /login; }, 3000); } });总结安全实践重要程度说明httpOnly Cookie优先高从根源防范XSS窃取Token有效期控制高Access Token 1h内Refresh Token动态轮换刷新令牌轮换高一旦使用旧refresh token吊销全部令牌CORS白名单中明确允许的跨域来源Token加密存储中localStorage加密后再存储多来源提取Token中Cookie/Header多种方式兼容SameSite策略高防止CSRF攻击令牌重用检测高发现异常立即吊销跨域认证没有银弹每种方案都有其适用场景和局限性。关键是理解不同存储和传输方式的安全特性根据业务需求选择合适的组合方案并在开发过程中始终将安全放在首位。