news 2026/10/14 9:27:32

登录认证这层皮:qkl-boot 的 JWT + Redis 会话,签发校验一次说清

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
登录认证这层皮:qkl-boot 的 JWT + Redis 会话,签发校验一次说清

登录认证这层皮:qkl-boot 的 JWT + Redis 会话,签发校验一次说清

导读:登录接口人人会写,但"token 哪来的、怎么验、失效了怎么办"——三句话能讲清的项目不多。qkl-boot 用的是 JWT(jjwt 0.12.6)+ Redis 双保险:JWT 负责无状态校验,Redis 负责可控失效。这套设计把"签发、校验、踢人、续期"四个场景都覆盖了,直接说实现。

先说为什么 JWT 要配 Redis。纯 JWT 的问题很经典:签发出去就管不住了。用户改密码、管理员封号、异地登录踢人,token 还在有效期内就还能用,除非等它自然过期。Redis 里存一份 session 记录,校验时"JWT 合法 + Redis 里有这条会话"双通过,要失效直接删 Redis,立刻生效。代价是多一次 Redis 查询,换取可控性——登录这种低频接口,值。

签发:登录成功,JWT + Redis 一起写

登录接口只做三件事:校验账号密码、生成 JWT、写 Redis 会话。密钥和有效期放配置,别写死在代码里:

# application.ymlqkl:security:jwt-secret:${JWT_SECRET:qkl-boot-default-secret-change-me-in-prod-0123456789}access-token-ttl:7200# token 有效期:秒,2 小时refresh-token-ttl:604800# 刷新有效期:7 天

签发逻辑:

@ServicepublicclassAuthService{@Value("${qkl.security.jwt-secret}")privateStringjwtSecret;publicLoginVOlogin(LoginDTOdto){// 1. 校验账号密码(这里用 MyBatis-Plus 查库,密码是 BCrypt 加密)Useruser=userMapper.selectOne(newLambdaQueryWrapper().eq(User::getUsername,dto.getUsername()));if(user==null||!BCrypt.checkpw(dto.getPassword(),user.getPassword())){thrownewBizException("账号或密码错误");}// 2. 签发 JWT:payload 里只放必要信息Stringtoken=Jwts.builder().subject(String.valueOf(user.getId())).claim("role",user.getRole()).issuedAt(newDate()).expiration(newDate(System.currentTimeMillis()+accessTtl*1000)).signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))).compact();// 3. 写 Redis 会话:key 带用户 id,value 是 token 摘要redis.set("login:token:"+user.getId(),token,accessTtl,TimeUnit.SECONDS);returnLoginVO.of(token,user);}}

payload 里别塞太多东西,用户角色、id 够用了。密码、手机号这些敏感信息进 JWT 是大忌——JWT 默认不加密,base64 一解码全裸奔。

校验:JWT 验签 + Redis 比对,双保险

Spring Boot 里做认证有两种路子:Filter 硬编码,或者集成 Spring Security 的 OncePerRequestFilter。qkl-boot 用的 Filter 方案,轻量、侵入小:

@ComponentpublicclassJwtAuthFilterextendsOncePerRequestFilter{@OverrideprotectedvoiddoFilterInternal(HttpServletRequestrequest,HttpServletResponseresponse,FilterChainchain)throwsServletException,IOException{Stringheader=request.getHeader("Authorization");if(header!=null&&header.startsWith("Bearer ")){Stringtoken=header.substring(7);try{// 1. 验签 + 解析 payloadClaimsclaims=Jwts.parser().verifyWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))).build().parseSignedClaims(token).getPayload();LonguserId=Long.valueOf(claims.getSubject());// 2. Redis 比对:库里没有这条会话 = 已被踢下线Stringcached=redis.get("login:token:"+userId);if(token.equals(cached)){request.setAttribute("userId",userId);request.setAttribute("role",claims.get("role"));}else{thrownewBizException("登录已失效,请重新登录");}}catch(JwtException|BizExceptione){response.setStatus(401);response.getWriter().write("{\"code\":401,\"msg\":\"token 无效或已过期\"}");return;}}chain.doFilter(request,response);}}

双保险的意义在这:JWT 验签保证"token 是真的",Redis 比对保证"这条会话还活着"。改密码后把 Redis 删掉,旧 token 验签照样通过,但 Redis 比对失败,直接 401——这就是可控性。

失效与续期:踢人和刷新是两个动作

踢人(强制下线)就是删 Redis,一条命令的事:

// 管理员封号 / 用户改密码 / 异地登录顶号publicvoidkickOut(LonguserId){redis.delete("login:token:"+userId);}

续期(刷新)要小心一个细节:刷新不能只重签 JWT,Redis 也得同步更新,否则新旧 token 对不上,前端拿新 token 请求还是 401:

publicStringrefresh(LonguserId,StringoldToken){Stringcached=redis.get("login:token:"+userId);if(!oldToken.equals(cached)){thrownewBizException("刷新凭证无效");}StringnewToken=Jwts.builder().subject(String.valueOf(userId)).issuedAt(newDate()).expiration(newDate(System.currentTimeMillis()+accessTtl*1000)).signWith(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8))).compact();// 关键:Redis 里的会话同步换新redis.set("login:token:"+userId,newToken,accessTtl,TimeUnit.SECONDS);returnnewToken;}

滑动续期:活跃用户不用频繁重新登录

2 小时有效期对"用一下就走"的场景够用,但招聘、零工这类用户会挂一整天。天天弹"登录已过期"体验太差。qkl-boot 的做法是滑动续期:token 剩余有效期小于阈值(比如 30 分钟)时,下次请求自动换发新 token,Redis 会话同步更新,用户无感知:

// 认证 Filter 里,校验通过后顺手做续期判断if(tokenTtlRemaining<1800){// 剩余不足 30 分钟StringnewToken=authService.refresh(userId,token);response.setHeader("X-New-Token",newToken);// 新 token 放响应头}
// 前端 axios 响应拦截器:收到新 token 自动替换并重放service.interceptors.response.use((res)=>{constnewToken=res.headers['x-new-token'];if(newToken){localStorage.setItem('token',newToken);res.config.headers.Authorization='Bearer '+newToken;returnservice.request(res.config);// 用新 token 重发原请求}returnres;});

注意滑动续期只在"剩余时间不足"时才换——每次都换 token,Redis 写操作翻好几倍,而且前端频繁替换 token 容易踩到并发请求用旧 token 的竞态。只在阈值边缘续一次,成本和体验都平衡。

踩坑实录:改完密码,老 token 还能用 2 小时

现象:用户在前台改了密码,立刻用旧密码的 token 调接口,居然还能正常访问。安全同事当场质疑:改密码后旧凭证应该立即失效。

排查过程:先看认证 Filter,验签 + Redis 比对都执行了。再看 Redis key——发现login:token:{userId}这条会话根本没有在改密码时被删。翻修改密码的 service 代码,只做了UPDATE user SET password=...,压根没调踢人逻辑。JWT 2 小时有效期内,Redis 会话一直在,老 token 自然畅通无阻。

定位思路:问题不在认证链路,在业务操作没有联动会话失效。改密码、封号、强制退出,这些动作都应该统一走"删除会话"逻辑,但当时的代码里只有登录会写会话,没有任何地方删会话。

最终解决:抽象一个会话管理服务,改密码、封号、退出登录统一调SessionManager.invalidate(userId):

@ServicepublicclassSessionManager{publicvoidinvalidate(LonguserId){redis.delete("login:token:"+userId);// 顺便把刷新 token 的 key 也删了,双保险redis.delete("refresh:token:"+userId);}}
// 修改密码时userMapper.updatePassword(userId,BCrypt.hashpw(newPwd,BCrypt.gensalt()));sessionManager.invalidate(userId);// 旧 token 立刻失效

改完复测:改密码 → 旧 token 请求 → 401,新登录拿新 token 正常。凡是"凭证不该再用"的业务动作,都必须联动失效会话,这个约定要写进团队规范。

可以直接抄的结论

  1. JWT payload 只放 id + role,敏感信息不进 token;
  2. 认证 Filter:验签(JWT)+ Redis 会话比对(可控失效)双保险;
  3. 踢人 = 删 Redis 会话,一条命令生效,不用等 token 过期;
  4. 刷新 token 必须 Redis 同步换新,否则新旧不一致会 401;
  5. 改密码、封号、退出登录统一走SessionManager.invalidate,别散落在各业务代码里;
  6. 密钥走环境变量注入,默认值只用于本地开发,上生产必须改。

JWT 给了你无状态,Redis 给了你可控。两个一拼,登录认证这层皮才算完整——既快,又管得住。

项目源码:https://gitee.com/gzqkl/qkl-boot

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 9:27:29

手机秒变电脑摄像头:DroidCamX连接方式与画质调优实战

1. 项目概述&#xff1a;当手机成为电脑的眼睛先说个场景&#xff0c;你肯定遇到过&#xff1a;视频会议马上开始&#xff0c;老板的脸已经怼在屏幕上&#xff0c;但你的电脑要么没有摄像头&#xff0c;要么画质糊得像蒙了一层保鲜膜。有人转头去买独立摄像头&#xff0c;几百块…

作者头像 李华
网站建设 2026/10/14 9:27:04

基于Python+Django+Vue的智能错题管理系统开发实践

1. 错题管理系统设计与实现概述 作为一名长期从事教育科技领域开发的工程师&#xff0c;我深知错题管理对学生学习效率提升的重要性。传统纸质错题本存在查找困难、分类不便、容易遗失等问题。基于PythonDjangoVue.js的错题管理系统&#xff0c;通过数字化手段解决了这些痛点&a…

作者头像 李华
网站建设 2026/10/14 9:26:59

Unity离线命令词识别实战:音频采集、资源加载与状态管理

1. 项目概述&#xff1a;为什么离线命令词识别在Unity中不是“加个SDK”就完事了“Unity端接入讯飞语音识别的离线命令词识别&#xff08;二&#xff09;”——这个标题里藏着三个关键信号&#xff1a;Unity是运行环境&#xff0c;讯飞语音识别是能力提供方&#xff0c;而离线命…

作者头像 李华
网站建设 2026/10/14 9:26:22

Agent技能库实战:从Prompt补丁到可复用能力编排

做Agent应用这两年年&#xff0c;我最大的感受不是模型不够聪明&#xff0c;而是能力太难复用。同一个“查一下客户历史工单”的操作&#xff0c;在不同流程里被我用Prompt写了七八遍&#xff0c;每次写出来行为还不一样——有时模型记得先过滤已关闭工单&#xff0c;有时就忘了…

作者头像 李华
网站建设 2026/10/14 9:26:14

商业卫星星座爆发增长与太空交通管理挑战

1. 太空竞赛新篇章&#xff1a;商业卫星星座的爆发式增长上周日本凯洛斯通信卫星的意外解体事件&#xff0c;再次将公众视线聚焦到近地轨道日益拥挤的现状。这颗价值数亿日元的卫星在轨解体后&#xff0c;产生的数百块碎片使得附近轨道区域的碰撞风险系数骤增30%。这已是今年第…

作者头像 李华
网站建设 2026/10/14 9:25:56

主动配电网中源-荷-储协同优化调度技术解析

1. 项目背景与研究意义 在能源转型的大背景下&#xff0c;电力系统正经历着从传统集中式供电模式向分布式、智能化方向的深刻变革。这种变革带来的最显著特征就是配电网中出现了大量分布式电源&#xff08;如光伏、风电&#xff09;、柔性负荷和储能装置&#xff0c;使得传统单…

作者头像 李华