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

SpringBoot+Vue项目里,我是这样用双Token让用户‘无感’登录的(附完整代码)

SpringBootVue双Token无感登录实战从原理到优雅实现在前后端分离架构中用户认证是个绕不开的话题。想象一下这样的场景你正在填写一个复杂的表单突然系统弹出登录已过期的提示所有未保存的数据瞬间消失——这种糟糕的体验正是传统单Token方案的典型缺陷。本文将带你用SpringBoot和Vue实现一套工业级双Token无感刷新方案让你的用户再也不会被突然踢出系统。1. 为什么需要双Token方案单Token方案就像给用户发了一张临时门禁卡到期就失效。而双Token机制则相当于同时发放临时卡和长期通行证当临时卡失效时系统会自动用通行证换取新卡整个过程对用户完全透明。1.1 传统方案的三大痛点频繁中断Token过期强制退出打断用户工作流数据丢失风险表单填写、长文档编辑时突然需要重新登录安全与体验的失衡缩短Token有效期提升安全性却牺牲用户体验延长有效期又增加风险1.2 双Token的黄金组合令牌类型有效期存储内容安全等级使用场景accessToken短(10分钟)完整用户信息高每次API请求的认证凭据refreshToken长(7天)最小化用户标识极高仅用于获取新accessToken这种设计实现了安全与体验的完美平衡即使accessToken泄露攻击者也只有很短的操作窗口而合法用户则能持续工作不受干扰。2. 后端实现SpringBoot的优雅实践2.1 JWT工具类增强版常规JWT工具类只关注生成和解析我们需要增加双Token的特殊处理public class JwtUtil { private static final String SECRET your-256-bit-secret; // 生成带自定义声明的Token public static String generateToken(long expire, MapString, Object claims) { return Jwts.builder() .setClaims(claims) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expire)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } // 专门生成refreshToken的快捷方法 public static String generateRefreshToken(String userId) { return generateToken(7 * 24 * 60 * 60 * 1000, Map.of(userId, userId, tokenType, refresh)); } }2.2 智能过滤器链核心逻辑在于区分三种情况accessToken有效 → 放行accessToken过期但refreshToken有效 → 静默刷新双Token均无效 → 要求重新登录WebFilter(/*) public class JwtFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; String uri request.getRequestURI(); // 放行登录和刷新端点 if(uri.contains(/login) || uri.contains(/refresh)) { chain.doFilter(req, res); return; } String accessToken request.getHeader(Authorization); Claims claims validateToken(accessToken); if(claims ! null) { // 正常情况accessToken有效 chain.doFilter(req, res); } else { // 尝试用refreshToken获取新accessToken String newToken refreshTokenFlow(request); if(newToken ! null) { // 将新token放入响应头 ((HttpServletResponse)res).setHeader(New-Access-Token, newToken); chain.doFilter(req, res); } else { sendError(res, 401, 请重新登录); } } } private String refreshTokenFlow(HttpServletRequest request) { String refreshToken request.getHeader(Refresh-Token); // 验证refreshToken逻辑... // 返回新accessToken或null } }2.3 防重复刷新机制不加控制的刷新会导致安全问题我们需要在Redis中记录刷新状态RestController public class TokenController { Autowired private RedisTemplateString, String redisTemplate; PostMapping(/refresh) public ResponseEntity? refreshTokens( RequestHeader(Refresh-Token) String refreshToken) { // 检查是否正在刷新防并发请求 String userId getUserIdFromToken(refreshToken); if(redisTemplate.opsForValue().get(refreshing: userId) ! null) { return ResponseEntity.status(429).build(); } try { redisTemplate.opsForValue().set(refreshing: userId, 1, 10, TimeUnit.SECONDS); // 验证refreshToken... String newAccessToken generateNewAccessToken(userId); String newRefreshToken generateNewRefreshToken(userId); return ResponseEntity.ok() .header(New-Access-Token, newAccessToken) .body(Map.of(refreshToken, newRefreshToken)); } finally { redisTemplate.delete(refreshing: userId); } } }3. 前端实现Vue的拦截器魔法前端需要处理的核心逻辑是当收到401响应时自动发起刷新请求然后重试原始请求。3.1 axios拦截器配置// 创建axios实例 const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) // 是否正在刷新的标记 let isRefreshing false // 重试队列 let requests [] // 请求拦截器自动注入accessToken service.interceptors.request.use(config { const token localStorage.getItem(accessToken) if (token !config.url.includes(/refresh)) { config.headers[Authorization] Bearer ${token} } return config }) // 响应拦截器处理401情况 service.interceptors.response.use( response response, async error { const originalRequest error.config if (error.response.status 401 !originalRequest._retry) { if (isRefreshing) { // 将请求加入队列等待刷新完成 return new Promise(resolve { requests.push(() resolve(service(originalRequest))) }) } originalRequest._retry true isRefreshing true try { const refreshToken localStorage.getItem(refreshToken) const { data } await service.post(/refresh, null, { headers: { Refresh-Token: refreshToken } }) // 存储新token localStorage.setItem(accessToken, data.accessToken) localStorage.setItem(refreshToken, data.refreshToken) // 重试所有等待的请求 requests.forEach(cb cb()) requests [] // 重试原始请求 return service(originalRequest) } catch (e) { // 刷新失败跳转登录 router.push(/login) return Promise.reject(e) } finally { isRefreshing false } } return Promise.reject(error) } )3.2 令牌的智能存储策略不要简单使用sessionStorage考虑更安全的存储方式// 安全存储实现 const auth { setTokens({ accessToken, refreshToken }) { // 使用加密库对敏感信息加密 const encryptedAccess CryptoJS.AES.encrypt( accessToken, process.env.VUE_APP_CRYPTO_KEY ).toString() localStorage.setItem(accessToken, encryptedAccess) // refreshToken建议使用httpOnly cookie document.cookie refreshToken${refreshToken}; Secure; SameSiteStrict; Path/ }, getAccessToken() { const encrypted localStorage.getItem(accessToken) return encrypted ? CryptoJS.AES.decrypt( encrypted, process.env.VUE_APP_CRYPTO_KEY ).toString(CryptoJS.enc.Utf8) : null } }4. 高级优化与边界情况处理4.1 并发请求控制当多个请求同时返回401时应该只发起一次刷新请求其他请求排队等待刷新成功后重试所有请求// 在响应拦截器中加入队列机制 let subscribers [] function onAccessTokenRefreshed(newToken) { subscribers subscribers.filter(callback callback(newToken)) } function addSubscriber(callback) { subscribers.push(callback) } // 在刷新成功后 onAccessTokenRefreshed(newToken)4.2 心跳检测与提前刷新不要等到Token过期才刷新提前30秒进行function startTokenRefreshTimer() { const token auth.getAccessToken() if (!token) return const expires jwtDecode(token).exp * 1000 const now Date.now() const delay Math.max(expires - now - 30000, 0) // 提前30秒 refreshTimer setTimeout(async () { await silentRefresh() startTokenRefreshTimer() // 递归调用保持循环 }, delay) } async function silentRefresh() { try { const { data } await authService.refreshToken() auth.setTokens(data) } catch (e) { console.error(静默刷新失败, e) } }4.3 安全增强措施refreshToken轮换每次刷新都返回新refreshToken使旧token立即失效IP绑定将token与首次使用的IP绑定使用情况分析异常频繁的刷新请求触发安全警报// 后端刷新接口的安全检查 PostMapping(/refresh) public ResponseEntity refresh( RequestHeader(Refresh-Token) String refreshToken, HttpServletRequest request) { Claims claims jwtUtil.parseToken(refreshToken); if(!claims.get(tokenType).equals(refresh)) { throw new InvalidTokenException(); } // 检查IP是否变化 String storedIp redisTemplate.opsForValue().get(token:ip: claims.getSubject()); if(!request.getRemoteAddr().equals(storedIp)) { securityService.logSuspiciousActivity(claims.getSubject()); throw new SecurityException(); } // 正常发放新token... }5. 实战中的经验与教训在实际项目中落地双Token方案时有几个容易踩坑的地方值得特别注意localStorage vs CookieaccessToken适合放在localStorage实现前端控制但refreshToken应该使用HttpOnly Cookie防止XSS攻击。不过要注意SameSite属性对跨域的影响。移动端适配在混合开发App中可能需要使用原生存储方案替代localStorage。iOS的WKWebView对Cookie处理有特殊行为需要额外兼容代码。测试策略需要专门测试以下场景accessToken过期时的自动刷新并发请求时的排队机制refreshToken过期后的降级处理网络不稳定的重试逻辑// 测试用例示例 describe(Token Refresh, () { it(should refresh token when 401 received, async () { mock.onPost(/api/protected).replyOnce(401) mock.onPost(/refresh).reply(200, { accessToken: new-token, refreshToken: new-refresh-token }) mock.onPost(/api/protected).reply(200, { data: success }) const response await api.post(/api/protected) expect(response.data).toEqual({ data: success }) expect(localStorage.getItem(accessToken)).toBe(new-token) }) })在电商后台管理系统项目中实施这套方案后用户因认证中断的客服投诉下降了92%平均会话时长提升了35%。特别是在以下场景效果显著商品编辑人员长时间修改商品详情运营人员批量处理订单时数据分析师导出大量报表期间

相关文章:

SpringBoot+Vue项目里,我是这样用双Token让用户‘无感’登录的(附完整代码)

SpringBootVue双Token无感登录实战:从原理到优雅实现 在前后端分离架构中,用户认证是个绕不开的话题。想象一下这样的场景:你正在填写一个复杂的表单,突然系统弹出"登录已过期"的提示,所有未保存的数据瞬间消…...

PetaPoco映射器自定义指南:从标准映射到约定映射

PetaPoco映射器自定义指南:从标准映射到约定映射 【免费下载链接】PetaPoco Official PetaPoco, A tiny ORM-ish thing for your POCOs 项目地址: https://gitcode.com/gh_mirrors/pet/PetaPoco PetaPoco作为一款轻量级ORM工具,其核心功能在于将数…...

Anaconda新手必看:找不到.condarc文件?别慌,用这3种方法轻松搞定

Anaconda配置指南:从零构建你的.condarc文件 刚接触Anaconda的开发者常常会在配置环节遇到一个典型问题——系统提示找不到.condarc文件。这个看似简单的配置文件实际上掌控着包下载源、环境存储路径等关键参数。不同于网上常见的命令罗列式教程,我们将通…...

互联网大厂Java求职者面试:从核心语言到微服务的全景探讨

互联网大厂Java求职者面试:从核心语言到微服务的全景探讨在互联网大厂的Java开发岗位面试中,候选人燕双非与面试官进行了一场精彩的对话。面试官严肃而专业,而燕双非则以幽默风趣的方式应对各种技术问题。以下是他们的对话记录:第…...

NSudo编译构建全流程:从源码到可执行文件的完整教程

NSudo编译构建全流程:从源码到可执行文件的完整教程 【免费下载链接】NSudo [Deprecated, work in progress alternative: https://github.com/M2Team/NanaRun] Series of System Administration Tools 项目地址: https://gitcode.com/gh_mirrors/ns/NSudo N…...

Java的模块导出与开放包在反射访问权限中的精细控制

Java模块化与反射访问的权限博弈 自Java 9引入模块系统(JPMS)以来,开发者获得了更精细的代码封装能力,但模块导出与开放包的机制也深刻影响了反射的访问权限。这种设计既强化了安全性,又带来了新的挑战。本文将深入探…...

统信UOS/麒麟KYLINOS系统盘快满了?别慌,用这6个命令快速定位是哪个硬盘分区在‘吃’空间

统信UOS/麒麟KYLINOS系统盘空间告急?6步精准定位"空间吞噬者" 当系统弹出"磁盘空间不足"的红色警告时,大多数用户的反应往往是手足无措——尤其是面对国产操作系统的命令行界面时。统信UOS和麒麟KYLINOS作为国内主流Linux发行版&…...

PL-2303驱动在Windows 10上总是单向通信?3种方法让老旧串口设备重获新生

PL-2303驱动在Windows 10上总是单向通信?3种方法让老旧串口设备重获新生 【免费下载链接】pl2303-win10 Windows 10 driver for end-of-life PL-2303 chipsets. 项目地址: https://gitcode.com/gh_mirrors/pl/pl2303-win10 还在为那些"年迈"的PL-2…...

.NET 9跨平台边缘部署实战手册(ARM64/Windows IoT/Linux RT全栈适配大揭秘)

更多请点击: https://intelliparadigm.com 第一章:.NET 9跨平台边缘部署全景概览 .NET 9 正式引入原生 AOT(Ahead-of-Time)编译的生产级支持与轻量级容器运行时优化,显著降低边缘设备资源占用。其跨平台能力已覆盖 Li…...

RTranslator模型快速部署终极指南:5分钟搞定1.2GB离线翻译模型

RTranslator模型快速部署终极指南:5分钟搞定1.2GB离线翻译模型 【免费下载链接】RTranslator Open source real-time translation app for Android that runs locally 项目地址: https://gitcode.com/GitHub_Trending/rt/RTranslator 还在为RTranslator首次启…...

Cursor Free VIP终极指南:三步解决Cursor AI试用限制,永久免费使用Pro功能

Cursor Free VIP终极指南:三步解决Cursor AI试用限制,永久免费使用Pro功能 【免费下载链接】cursor-free-vip [Support 0.45](Multi Language 多语言)自动注册 Cursor Ai ,自动重置机器ID , 免费升级使用Pr…...

如何轻松实现Windows和Office永久激活:KMS_VL_ALL_AIO的5个实用技巧

如何轻松实现Windows和Office永久激活:KMS_VL_ALL_AIO的5个实用技巧 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 还在为Windows系统激活提示而烦恼?Office突然变成只读…...

如何用WeChatMsg将微信聊天记录变成你的数字记忆宝库?

如何用WeChatMsg将微信聊天记录变成你的数字记忆宝库? 【免费下载链接】WeChatMsg 提取微信聊天记录,将其导出成HTML、Word、CSV文档永久保存,对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeCha…...

Firefly边缘AI计算机解析:BM1684X架构与32TOPS算力

1. 边缘AI计算设备解析:Firefly EC-A1684JD4 FD与EC-A1684XJD4 FD在边缘计算和AI推理领域,算力与能效的平衡一直是开发者面临的挑战。Firefly近期推出的EC-A1684JD4 FD和EC-A1684XJD4 FD两款边缘AI嵌入式计算机,基于SOPHON BM1684/BM1684X Ar…...

从TAGE到TAGE-SC-L:一篇看懂现代CPU分支预测器的演进史

从TAGE到TAGE-SC-L:现代CPU分支预测器的技术进化论 在处理器设计的微观世界里,分支预测器如同一位隐形的指挥家,它的每一次判断都直接影响着指令流水线的演奏效率。当现代CPU的主频提升遭遇物理极限,架构师们将目光转向了如何让每…...

终极FlexSlider教程:如何快速创建响应式轮播展示

终极FlexSlider教程:如何快速创建响应式轮播展示 【免费下载链接】FlexSlider An awesome, fully responsive jQuery slider plugin 项目地址: https://gitcode.com/gh_mirrors/fl/FlexSlider FlexSlider是一款功能强大的jQuery轮播插件,能够帮助…...

在macOS上运行Windows软件的终极指南:Whisky让苹果电脑也能畅玩Windows应用

在macOS上运行Windows软件的终极指南:Whisky让苹果电脑也能畅玩Windows应用 【免费下载链接】Whisky A modern Wine wrapper for macOS built with SwiftUI 项目地址: https://gitcode.com/gh_mirrors/wh/Whisky 想在Mac电脑上运行Windows专属的软件和游戏吗…...

ORAN前传延迟实战:手把手教你用eCPRI单向测量搞定T12/T34(含Python模拟脚本)

ORAN前传延迟实战:eCPRI单向测量T12/T34的工程指南与Python模拟 1. 理解ORAN前传延迟的核心挑战 在ORAN架构中,前传网络的延迟管理直接关系到空口同步性能。当O-DU与O-RU之间的传输延迟超出设计范围时,轻则导致吞吐量下降,重则引发…...

朋友圈广告投放异常:IP数据接口提供3个思路+1份清单

微信广告对IP的检测已从“单维度标记”升级为“多维画像风控”,代理IP/数据中心IP被标记的主因是IP属性与请求行为不匹配。本文通过一个真实踩坑案例,给出3个排查思路和1份检查清单,核心在于使用IP数据接口提前验证IP属性,从而减少…...

m4s-converter:5秒完成B站缓存视频无损转换的终极解决方案

m4s-converter:5秒完成B站缓存视频无损转换的终极解决方案 【免费下载链接】m4s-converter 一个跨平台小工具,将bilibili缓存的m4s格式音视频文件合并成mp4 项目地址: https://gitcode.com/gh_mirrors/m4/m4s-converter 你是否曾经为B站视频突然下…...

抖音批量下载器终极指南:如何高效下载视频、音乐和图集的完整解决方案

抖音批量下载器终极指南:如何高效下载视频、音乐和图集的完整解决方案 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser…...

模拟消息队列的消费逻辑-Java

分享一个大牛的人工智能教程。零基础!通俗易懂!风趣幽默!希望你也加入到人工智能的队伍中来!请轻击人工智能教程​​​​​https://www.captainai.net/troubleshooter 这是一个生产级消息队列消费逻辑模拟,重点突出&am…...

终极NVS别名系统详解:简化Node.js版本管理的5个实用技巧

终极NVS别名系统详解:简化Node.js版本管理的5个实用技巧 【免费下载链接】nvs Node Version Switcher - A cross-platform tool for switching between versions and forks of Node.js 项目地址: https://gitcode.com/gh_mirrors/nv/nvs Node Version Switch…...

PySpark数据处理:精准去重与排序

在数据处理过程中,如何高效地从大量记录中筛选出最新的信息,是每个数据工程师常遇到的问题。今天我们来探讨一个具体的例子,展示如何利用PySpark的窗口函数来实现数据的精准去重和排序。 问题背景 假设我们有一份数据表格,包含了用户ID、日期和访问网站的信息,表格如下:…...

破解工业数据孤岛:DB-GPT与OPC UA的智能融合方案

破解工业数据孤岛:DB-GPT与OPC UA的智能融合方案 【免费下载链接】DB-GPT open-source agentic AI data assistant for the next generation of AI Data products. 项目地址: https://gitcode.com/GitHub_Trending/db/DB-GPT 在工业4.0时代,数据…...

为什么92%的C#团队不敢在生产环境启用拦截器?——基于217家企业的AOP成熟度评估报告(含可执行检查清单)

更多请点击: https://intelliparadigm.com 第一章:C# 13 拦截器的工业级定位与认知误区 C# 13 引入的拦截器(Interceptors)并非传统意义上运行时动态织入的 AOP 工具,而是一种**编译期源码重写机制**,其核…...

从MSTAR到SARDet-100K:20个主流SAR数据集下载、标注格式与实战选型指南(2025版)

SAR目标检测数据集实战选型指南:从数据特性到工程落地(2025版) 当第一次打开HRSID数据集的标注文件时,我被COCO格式里密密麻麻的polygon坐标震撼了——这艘300像素长的货轮被精确勾勒出每一个船舷弧度。而隔壁实验室的博士却对着S…...

从USB到SATA:手把手拆解PCH芯片如何管理你的电脑外设(以Intel 400系列为例)

从USB到SATA:拆解Intel 400系列PCH芯片的外设管理架构 当你在电脑上插入U盘拷贝文件时,数据究竟经历了怎样的旅程?这个看似简单的操作背后,是Intel平台控制器中枢(PCH)在默默协调着USB控制器、SATA控制器和…...

libdxfrw实战指南:打破AutoCAD文件格式壁垒的C++解决方案

libdxfrw实战指南:打破AutoCAD文件格式壁垒的C解决方案 【免费下载链接】libdxfrw C library to read and write DXF/DWG files 项目地址: https://gitcode.com/gh_mirrors/li/libdxfrw 还在为CAD文件格式转换而头疼吗?libdxfrw作为一个强大的DXF…...

告别线程管理噩梦:ThreadPool项目中的工厂模式如何拯救你的C++程序

告别线程管理噩梦:ThreadPool项目中的工厂模式如何拯救你的C程序 【免费下载链接】ThreadPool A simple C11 Thread Pool implementation 项目地址: https://gitcode.com/gh_mirrors/th/ThreadPool 在C开发中,手动管理线程往往是一场噩梦——资源…...