当前位置: 首页 > news >正文

JWT令牌在多端跨域场景下的安全访问校验实践

JWT令牌在多端跨域场景下的安全访问校验实践

跨域认证的挑战

现代前端应用往往需要服务于多个端:Web端、移动端H5、小程序、第三方嵌入等。不同端的域名、运行环境和存储策略各不相同,如何在保证安全的前提下实现统一的身份认证,是每个前端架构师都需要面对的问题。

JWT(JSON 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 Cookie(domain设置)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加密后再存储
多来源提取TokenCookie/Header多种方式兼容
SameSite策略防止CSRF攻击
令牌重用检测发现异常立即吊销

跨域认证没有银弹,每种方案都有其适用场景和局限性。关键是理解不同存储和传输方式的安全特性,根据业务需求选择合适的组合方案,并在开发过程中始终将安全放在首位。

http://www.zskr.cn/news/1449735.html

相关文章:

  • mpv.net 终极指南:Windows平台高性能媒体播放器完整配置与实战技巧
  • IDEA装了LiteFlowX插件后,我写规则文件再也没翻过文档(智能提示+跳转真香)
  • 如何5分钟搭建个人音乐库:洛雪音乐聚合音源终极指南
  • 别再只看风速了!固定翼新手选飞行天气,这3个APP和2个关键数据更重要
  • IPXWrapper完整指南:让Windows 10/11完美运行经典游戏联机
  • 鸣潮模组终极指南:15+功能解锁,彻底改变你的游戏体验
  • 电路设计入门:从元器件到实战项目,零基础掌握电子制作核心技能
  • OmenSuperHub完整指南:解锁惠普游戏本隐藏性能的终极工具
  • 【浏览器智能体】Browser Use 与现有 pytest-bdd 框架的深度整合方案
  • Unity游戏开发避坑:用.NET 4.x和System.Data.SqlClient搞定SQL Server 2022连接(保姆级教程)
  • 从卓晴到稚晖君:盘点那些硬核技术大佬的“神仙”个人实验室
  • 告别手动画框!用SurgicalSAM实现手术器械的“一句话分割”:从类提示到精准掩码的保姆级解析
  • JetBrains IDE试用期重置终极教程:简单快速恢复30天免费使用
  • 别只敲命令了!用Shell脚本把openEuler日常操作自动化(附5个实用脚本)
  • 从HUSTOJ迁移到Hydro OJ:一个老牌OJ维护者的踩坑与平滑升级指南
  • 告别WPS看图!用这个免费插件让Windows 10/11文件夹直接预览SVG图片
  • 大麦网演唱会抢票神器:Python自动化脚本告别黄牛高价票
  • 中牟沙发翻新换皮换布哪家好、匠阁、御匠、锦修三大品牌哪个靠谱公司推荐、怎么选沙发翻新服务商 - 卓一科技
  • 荥阳沙发翻新换皮换布哪家好、匠阁、御匠、锦修三大品牌哪个靠谱公司推荐、怎么选沙发翻新服务商 - 卓一科技
  • Streamlit开发LLM应用时,关于`st.session_state`和页面重渲染的3个关键陷阱
  • 2026年CAD转PDF完全教程:批量转换方法与AutoCAD导出详细步骤一看就会
  • 昆山装修公司设计师怎么选:从业年限与落地能力的判断逻辑 - 资讯焦点
  • 超越KITTI文档:深度拆解calib.txt,揭秘多相机标定数据在自动驾驶仿真中的真实用法
  • 保姆级避坑指南:Ubuntu 18.04上ROS Melodic安装全流程(含国内源与rosdep更新终极方案)
  • Android TV Leanback高级开发实战指南:架构设计与交互模式深度解析
  • YOLOv8模型在RK3588上部署的实战避坑:从ONNX导出到RKNN转换的关键步骤详解
  • 移动电源DIY改造:从IP5305电路分析到18650电池扩容实战
  • 技术文档可视化革命:Mermaid Live Editor如何重塑团队协作效率
  • 大语言模型聊天机器人的缺陷与应对:从幻觉、偏见到安全实践
  • AnolisOS 8.8安装源报错?别慌,三种解决方案(含U盘安装和离线配置)