SpringBoot实战:JWT Token 自动续期的解决方案

前言

在前后端分离的开发架构中,当用户成功登录后,后端服务会生成一个JWT(JSON Web Tokens)token,并将其返回给前端。前端(如Vue应用)接收到此token后,通常会将其存储在LocalStorage中以方便后续请求时使用。每次向后端发送请求时,前端会将这个token作为请求头的一部分发送给后端,以便后端通过自定义的过滤器或中间件进行身份验证。

考虑到JWT token中可能包含敏感的用户信息,并且出于安全考虑,这些token的过期时间往往被设置得相对较短。然而,较短的过期时间可能会带来用户体验上的问题,尤其是在用户进行长时间操作(如填写复杂表单)时,token可能会意外过期,导致用户需要重新登录,进而丢失已填写的数据,这对用户体验是极为不利的。

一、实现原理

在登录流程中,当用户成功认证后,我们可以将生成的JWT token作为键(key)和值(value)存储到缓存系统中,此时键和值相同是为了便于管理和查找。将缓存的有效期设置为JWT token有效期的两倍,这样可以提供一个缓冲期,用于在用户实际token过期后的一段时间内仍然能够验证其身份并可能自动续期。

后端系统通过JWT Filter来拦截每个请求,并验证前端发送的token是否有效。如果token无效,说明是非法请求,Filter将直接抛出异常或返回错误响应。

在JWT Filter中,除了验证前端发送的token之外,还会根据规则去缓存中尝试获取对应的cache token(即之前存储的、与前端token相同的token):

  1. cache token 不存在:
    这种情况表明自用户上次活动以来,缓存中的token已经过期或被清除,可能由于用户长时间未操作或系统缓存策略导致。此时,可以认为用户账户处于空闲超时状态,Filter应返回“用户信息已失效,请重新登录”的提示,并引导用户重新进行登录操作。

  2. cache token 存在:
    如果缓存中存在对应的token,则需要进一步使用JWT工具类来验证这个cache token是否已过期。

    • 未过期:如果cache token未过期,说明用户当前是活跃的,无需进行额外处理,继续处理请求即可。

    • 已过期:如果cache token已过期,但考虑到用户可能一直在进行操作只是token本身失效了,后端程序可以执行以下操作:首先,根据当前用户信息(可能需要从数据库或其他认证服务中获取)重新生成一个新的JWT token;然后,使用这个新的token覆盖缓存中原有的token值;最后,更新该缓存项的生命周期,重新开始计算。这样,用户在无需重新登录的情况下,可以继续他们的操作。

通过这种机制,我们能够在保证JWT token安全性的同时,提升用户体验,减少因token过期导致的频繁登录需求。

三、代码实现(伪码)

  1. 登录成功后给用户签发token,并设置token的有效期

  2. ...
    SysUser sysUser = userService.getUser(username,password);
    if(null !== sysUser){
        String token = JwtUtil.sign(sysUser.getUsername(),
    sysUser.getPassword());
    }
    ...
     
     
    public static String sign(String username, String secret) {
        //设置token有效期为30分钟
     Date date = new Date(System.currentTimeMillis() + 30 * 60 * 1000);
     //使用HS256生成token,密钥则是用户的密码
     Algorithm algorithm = Algorithm.HMAC256(secret);
     // 附带username信息
     return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
    }
  3. 将token存入redis,并设定过期时间,将redis的过期时间设置成token过期时间的两倍

  4. Sting tokenKey = "sys:user:token" + token;
    redisUtil.set(tokenKey, token);
    redisUtil.expire(tokenKey, 30 * 60 * 2);
  5. 过滤器校验token,校验token有效性

  6. public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        //从header中获取token
     String token = httpServletRequest.getHeader("token")
     if(null == token){
      throw new RuntimeException("illegal request,token is necessary!")
     }
        //解析token获取用户名
     String username = JwtUtil.getUsername(token);
     //根据用户名获取用户实体,在实际开发中从redis取
     User user = userService.findByUser(username);
        if(null == user){
      throw new RuntimeException("illegal request,token is Invalid!")
        }
     //校验token是否失效,自动续期
     if(!refreshToken(token,username,user.getPassword())){
      throw new RuntimeException("illegal request,token is expired!")
     }
     ...
    }
  7. 实现token的自动续期

  8. public boolean refreshToken(String token, String userName, String passWord) {
     Sting tokenKey = "sys:user:token" + token ;
     String cacheToken = String.valueOf(redisUtil.get(tokenKey));
     if (StringUtils.isNotEmpty(cacheToken)) {
      // 校验token有效性,注意需要校验的是缓存中的token
      if (!JwtUtil.verify(cacheToken, userName, passWord)) {
       String newToken = JwtUtil.sign(userName, passWord);
       // 设置超时时间
       redisUtil.set(tokenKey, newToken) ;
       redisUtil.expire(tokenKey, 30 * 60 * 2);
      }
      return true;
     }
     return false;
    }
    ...
     
    public static boolean verify(String token, String username, String secret) {
     try {
      // 根据密码生成JWT效验器
      Algorithm algorithm = Algorithm.HMAC256(secret);
      JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build();
      // 效验TOKEN
      DecodedJWT jwt = verifier.verify(token);
      return true;
     } catch (Exception exception) {
      return false;
     }
    }

 

posted on 2024-09-23 10:21  IT-QI  阅读(160)  评论(0编辑  收藏  举报