JWT 双 Token 机制实战:2 小时 + 7 天,让登录状态既安全又无感
JWT 双 Token 机制实战2 小时 7 天让登录状态既安全又无感 本文是我在视频推流项目中设计登录认证体系的完整复盘包含JWT Redis 白名单/黑名单 双 Token 续期 Token Rotation全套实现。配套源码仓库github.com/JiaqiChen3518/video_stream登录认证里有一个经典的两难Token 有效期设长比如 7 天用户体验好但 token 泄露后攻击者有整整 7 天的时间冒充用户Token 有效期设短比如 2 小时安全但用户每隔两小时就要重新登录一次体验稀碎。这篇文章讲我们项目的解法——双 Token 机制短时效的 access token 负责日常访问长时效的 refresh token 负责续命。外加一套 Redis 白名单/黑名单机制解决 JWT签发后无法撤销的天然缺陷。一、方案总览登录成功 ├──► access token有效期 2 小时── 每次请求携带负责身份认证 └──► refresh token有效期 7 天── 平时躺在前端只在续期时用 access 快过期 ├── 剩余 1 小时 → 过滤器自动续期用户无感知响应头带回新 token └── access 已失效 → 用 refresh token 换新Token Rotation旧的 refresh 同时作废二、JWT 本身30 秒回顾JWT 由三段组成Header.Payload.Signature。我们用它只存必要声明绝不存密码等敏感信息StringtokenJWT.create().withIssuer(ISSUER)// 签发者.withSubject(userId.toString())// 主题用户ID.withClaim(username,username).withClaim(role,role).withClaim(tokenId,tokenId)// ★ 全局唯一的token身份证后文的关键.withIssuedAt(now).withExpiresAt(expireTime)// 2小时后过期.sign(Algorithm.HMAC256(SECRET));// HMAC256签名防篡改JWT 的价值无状态、自包含——服务端不用查库就能验证身份验签名即可。但它有个著名缺陷签发出去就失控了。签名没过期的 token 永远有效想踢人下线用户改密码后作废旧 token都做不到。三、Redis 补救白名单 黑名单 多设备管理我们的方案JWT 照发但每个 token 生成时把tokenId登记到 Redis。验证时除了验签名还要查 Redis——用一点点状态换回完全的控制权// ① 生成 access token 时登记白名单StringtokenIdUUID.randomUUID().toString();StringtokenJWT.create()...withClaim(tokenId,tokenId)...sign(ALGORITHM);RedisUtil.set(token:access:tokenId,token,7200);// 白名单2小时有效RedisUtil.sadd(user:token:userId,tokenId,7200);// 用户→token集合支持多设备登录验证时做三重检查publicstaticbooleanverifyToken(Stringtoken){DecodedJWTjwtVERIFIER.verify(token);// 第一重签名过期时间StringtokenIdjwt.getClaim(tokenId).asString();if(RedisUtil.get(token:access:tokenId)null){// 第二重白名单被删被踢下线returnfalse;}if(isTokenInBlacklist(tokenId)){// 第三重黑名单returnfalse;}returntrue;}登出黑名单的 TTL 为什么恰好是剩余时间JWT 无法销毁但可以让它失效——把 tokenId 放进黑名单publicstaticvoidinvalidateToken(Stringtoken){DecodedJWTjwtdecodeToken(token);StringtokenIdjwt.getClaim(tokenId).asString();longremainMsjwt.getExpiresAt().getTime()-System.currentTimeMillis();if(remainMs0){// 黑名单过期时间 原token的剩余时间RedisUtil.set(token:blacklist:tokenId,1,(int)(remainMs/1000));RedisUtil.del(token:access:tokenId);// 顺手从白名单删除RedisUtil.srem(user:token:userId,tokenId);// 从用户的token集合移除}}黑名单 TTL token 剩余有效期的细节很妙token 反正 2 小时后自己过期黑名单没必要记一辈子——到期自动消失黑名单集合永远保持极小。四、自动续期让用户永远无感access token 只有 2 小时难道用户每小时都要被踢去登录不需要。我们在 JWT 过滤器里加一段滑动续期逻辑// JwtFilter.doFilter 中StringtokengetToken(request);// 从 Authorization: Bearer 中取if(!JwtUtil.verifyToken(token)){thrownewUnauthorizedException(Token无效或已过期);}// ★ 剩余有效期 1小时就地续期用户完全无感StringrenewedTokenJwtUtil.checkAndRenewToken(token);if(!renewedToken.equals(token)){response.setHeader(X-Renewed-Token,renewedToken);// 新token放响应头带回tokenrenewedToken;}// 解析用户信息 → 存入ThreadLocal → 放行UserContext.setUserId(userId);UserContext.setRole(role);续期逻辑本身publicstaticStringcheckAndRenewToken(Stringtoken){DecodedJWTjwtdecodeToken(token);longremainingjwt.getExpiresAt().getTime()-System.currentTimeMillis();if(remainingRENEWAL_THRESHOLDremaining0){// 阈值1小时invalidateToken(token);// 作废旧token进黑名单returngenerateAccessToken(userId,username,role);// 签发新token}returntoken;// 还早不动}设计上有三个讲究阈值续期1 小时而不是每次请求都换避免每个请求都签发新 token 写 Redis 的浪费续期 作旧 发新旧 token 进黑名单——如果旧 token 已被攻击者截获它立刻失效新 token 通过响应头X-Renewed-Token带回前端拦截器替换本地存储即可用户全程无感知。五、Token Rotationrefresh token 的重生如果用户 2 天没上线access token 彻底过期自动续期也救不了它只对还没死的 token 生效。此时前端拿出 7 天有效期的 refresh token 调/user/refresh换新publicstaticString[]refreshAccessToken(StringrefreshToken){DecodedJWTjwtVERIFIER.verify(refreshToken);StringoldTokenIdjwt.getClaim(tokenId).asString();// refresh token 也要查Redis白名单防伪造if(RedisUtil.get(token:refresh:oldTokenId)null)returnnull;invalidateRefreshToken(userId,oldTokenId);// ★ 旧的refresh token立即作废StringnewAccessTokengenerateAccessToken(userId,username,role);StringnewRefreshTokengenerateRefreshToken(userId);// ★ 连refresh一起换新returnnewString[]{newAccessToken,newRefreshToken};}这里用的是Token Rotation令牌轮换刷新时同时换发新的 access token 和 refresh token旧的 refresh token 立即作废。安全收益refresh token 有效期长达 7 天是攻击者的重点目标。轮换机制让偷到旧 refresh token 的攻击者只能用一次——下次用户刷新时旧 token 已失效攻击者用旧 token 刷新会失败可配合服务端检测失效 token 被使用的异常判定 token 泄露并强制全端下线每次刷新都轮换refresh token 的实际暴露窗口被压缩到两次刷新之间。六、过滤器链认证逻辑放哪ThreadLocal 怎么清整个认证在 Filter 层统一完成Controller 和 Service 里看不到一行 token 解析代码publicvoiddoFilter(ServletRequestrequest,ServletResponseresponse,FilterChainchain){// ① 白名单路径直接放行登录/注册/视频列表/搜索等公开接口if(isExcludePath(requestURI)){chain.doFilter(request,response);return;}// ② 取token → ③ 验证 → ④ 自动续期 → ⑤ 解析用户身份// 校验失败抛 UnauthorizedException由异常过滤器统一转JSON// ⑥ 身份存入ThreadLocal业务层通过 UserContext.getUserId() 随处可取UserContext.setUserId(userId);UserContext.setRole(role);try{chain.doFilter(request,response);}finally{UserContext.clear();// ★ 必须清理}}finally { UserContext.clear(); }是 Tomcat 线程复用陷阱的标准答案Tomcat 的线程池会复用工作线程如果不清 ThreadLocal上一个请求的用户信息会渗漏给下一个复用该线程的请求——A 用户可能读到 B 用户的身份。这类 bug 极难排查而防御只需要一行 finally。过滤器的顺序也有讲究我们的配置是异常处理 跨域(CORS) JWT。异常过滤器放最外圈保证任何一环抛出的异常包括 JWT 校验失败都能被统一兜底成 JSON 错误响应。七、完整时序一次登录后的一周服务端前端用户服务端前端用户alt[剩余1小时][剩余1小时]loop[日常请求]2小时后access过期7天未登录refresh也过期登录账号密码access(2h) refresh(7d)携带access token验签白名单黑名单正常响应响应 X-Renewed-Token前端替换用refresh换新旧refresh作废签发新双token重新登录八、设计取舍我们放弃了什么方案优点我们为什么没用纯 JWT无 Redis完全无状态验证零 IO无法踢人下线、无法强制失效改密码/token 泄露都无解Session Cookie服务端完全可控有状态、跨域麻烦、不适合前后端分离 App 多端JWT Redis 白名单本文兼顾 JWT 的简洁与服务端的控制权每次验证多一次 Redis 查询可用本地缓存白名单优化还有个务实的小决定验证要查 Redis理论上比纯 JWT 多一次 IO。但 access token 的验证 QPS 与业务接口同量级一次 Redis 查询微秒级换来随时踢人的能力对视频社区这种有管理后台的系统是划算的。九、面试快问快答Q1为什么需要双 Token单 Token 不行吗单 Token 的长短有效期矛盾无法调和长了不安全短了体验差。双 Token 把日常访问短和续期凭证长分离access 泄露窗口小refresh 使用频率低暴露少。Q2JWT 有什么缺点你们怎么解决最大缺点是无法撤销无状态的双刃剑。我们通过 tokenId Redis 白名单解决删除白名单 踢下线黑名单集合处理登出和续期作旧。代价是每次验证多一次 Redis 查询。Q3什么是 Token Rotation刷新令牌时同时轮换 access 和 refresh token旧 refresh 立即作废。安全收益refresh token 被盗后只能使用一次服务端还能通过失效 token 被使用检测泄露。Q4ThreadLocal 存用户信息有什么坑Tomcat 线程池复用线程请求结束必须remove()/clear()否则上一个请求的用户信息渗漏给下一个请求。务必写在 finally 里。Q5为什么黑名单要设置过期时间黑名单的寿命只需要覆盖原 token 的剩余有效期——token 自己过期后黑名单记录就是垃圾数据。设置剩余时间为 TTL黑名单集合可以永远保持极小无需清理任务。 本篇完整源码JwtUtil.java JwtFilter.java UserContext.java 下一篇预告《缓存穿透、击穿、雪崩——一次用真实代码讲透缓存三大问题》如果这篇对你有帮助欢迎点赞收藏关注

相关新闻

Coursebook exec详解:进程映像替换完全指南

Coursebook exec详解:进程映像替换完全指南

Coursebook exec详解:进程映像替换完全指南 【免费下载链接】coursebook Open Source Introductory Systems Programming Textbook for the University of Illinois 项目地址: https://gitcode.com/GitHub_Trending/co/coursebook 本文基于伊利诺伊大学开源系…

2026/10/4 7:49:10 阅读更多 →
GLiNER2.5-Decide进阶技巧:带描述的标签与0-10序数评分,精确驾驭私有分类体系

GLiNER2.5-Decide进阶技巧:带描述的标签与0-10序数评分,精确驾驭私有分类体系

GLiNER2.5-Decide进阶技巧:带描述的标签与0-10序数评分,精确驾驭私有分类体系 【免费下载链接】GLiNER2.5-Decide 项目地址: https://ai.gitcode.com/hf_mirrors/fastino/GLiNER2.5-Decide GLiNER2.5-Decide 是 GLiNER2.5 家族中的 340M 参数英文…

2026/10/4 7:48:10 阅读更多 →
基于PyTorch的CNN柑橘成熟度识别实战:从数据预处理到模型部署

基于PyTorch的CNN柑橘成熟度识别实战:从数据预处理到模型部署

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 7:48:10 阅读更多 →

最新新闻

Webots R2023b 版本深度解析:Pose 节点迁移、VacuumGripper 真空吸盘与外置控制器启动器实战

Webots R2023b 版本深度解析:Pose 节点迁移、VacuumGripper 真空吸盘与外置控制器启动器实战

科研自动驾驶物理引擎 【免费下载链接】webots Webots Robot Simulator 项目地址: https://gitcode.com/gh_mirrors/web/webots 点击查看 免费下载 Webots R2023b 是 Cyberbotics 于 2023 年 6 月 28 日发布的重大版本更新,本篇文章以官方发布文档 docs…

2026/10/4 9:04:12 阅读更多 →
国庆七天搭建飞书企微自动化工作流:WorkBuddy实战指南

国庆七天搭建飞书企微自动化工作流:WorkBuddy实战指南

1. 为什么我要在国庆七天折腾这套自动化工作流国庆七天假,说长不长说短不短。出门堵在高速上看车尾灯,不如在家把一直想搭但没时间搭的自动化工作流给落地了。我平时的工作状态是这样的:飞书里堆着各种需求文档和表格,企微里是团队…

2026/10/4 9:04:12 阅读更多 →
VFH避障算法原理与调参实战:从向量场直方图到机器人局部路径规划

VFH避障算法原理与调参实战:从向量场直方图到机器人局部路径规划

做机器人避障和局部路径规划的人,应该没有一个没听说过VFH。VFH算法全称是Vector Field Histogram(向量场直方图),它解决的是移动机器人在未知环境下,如何根据传感器信息实时避开障碍物并朝目标方向运动的问题。市面上…

2026/10/4 9:04:12 阅读更多 →
IT爱学堂-普通人玩转 Vibe Coding 实战课,Vibe Coding全栈开发实战课(完结)

IT爱学堂-普通人玩转 Vibe Coding 实战课,Vibe Coding全栈开发实战课(完结)

一句话生成项目:从自然语言到可运行系统的技术链路 普通人玩转 Vibe Coding,第一步不是让 AI 立刻写代码,而是学会把一句话变成可执行的规格。所谓一句话生成项目,背后并不是魔法,而是一条技术链路:意图澄清…

2026/10/4 9:04:12 阅读更多 →
Hey压测公共服务:压别人家API时的法律红线与礼仪规范

Hey压测公共服务:压别人家API时的法律红线与礼仪规范

Hey压测公共服务:压别人家API时的法律红线与礼仪规范 【免费下载链接】hey HTTP load generator, ApacheBench (ab) replacement 项目地址: https://gitcode.com/GitHub_Trending/he/hey Hey 是一个用 Go 编写的轻量级 HTTP 压测工具(ApacheBench…

2026/10/4 9:04:12 阅读更多 →
宿舍楼局域网组网实战:从拓扑选型到VLAN划分的完整方案

宿舍楼局域网组网实战:从拓扑选型到VLAN划分的完整方案

简介:一份面向网络工程、计算机及相关专业学生的完整课程设计文档,以大学学生宿舍楼为场景,系统梳理局域网从规划到运维的闭环流程。文档主体为1个doc文件,压缩包约2.26MB,内容围绕网络规划、网络设计、网络实施、测试…

2026/10/4 9:03:12 阅读更多 →

日新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 1:00:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:35 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/3 9:42:36 阅读更多 →