JWT的token过期自动续期(无redis)

思路:

由于jwt中的token过期时间是打包在token中的,用户登录以后发送给客户端以后token不能变化,那么要在用户无感知的情况下刷新token,就要在符合过期条件的情况下,在缓存一个新的token,作为续命token,再次解析不要解析客户端发送的token,要解析自己缓存的续命token

主要逻辑:

如果当前token没有超过过期时间的两倍,续期,超过了重新登录

主要代码如下:

package com.hongseng.app.config.jwtfilter;

import com.hongseng.app.config.exception.RefreshTokenException;
import enums.TokenEnum;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import utils.JwtTokenUtils;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Objects;

/**
 * @program: spring-security
 * @description: 验证成功当然就是进行鉴权了,每一次需要权限的请求都需要检查该用户是否有该权限去操作该资源,当然这也是框架帮我们做的,那么我们需要做什么呢?
 * 很简单,只要告诉spring-security该用户是否已登录,是什么角色,拥有什么权限就可以了。
 * @author: fbl
 * @create: 2020-12-02 14:25
 **/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
    private static final String LOGIN_URL = "/login";
    /**
     * 为每一个用户准备续命token
     */
    public static HashMap<String,String> TOKEN = new HashMap<>();

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws IOException, ServletException {

        String tokenHeader = request.getHeader(TokenEnum.TOKEN_HEADER.getValue());

        // 如果请求头中没有Authorization信息或者是登录接口直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(TokenEnum.TOKEN_PREFIX.getValue()) || request.getRequestURL().toString().contains(LOGIN_URL)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        String token = tokenHeader.replace(TokenEnum.TOKEN_PREFIX.getValue(), "");
        String userName = JwtTokenUtils.getUsername(token);

        try {
            if (JWTAuthorizationFilter.TOKEN.get(userName) != null) {
                refreshToken(TOKEN.get(userName));
                SecurityContextHolder.getContext().setAuthentication(getAuthentication(TOKEN.get(userName)));
            } else {
                refreshToken(tokenHeader);
                SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
            }
        } catch (RefreshTokenException | ExpiredJwtException e) {
            // 异常捕获,发送到expiredJwtException
            request.setAttribute("expiredJwtException", e);
            //将异常分发到/expiredJwtException控制器
            request.getRequestDispatcher("/expiredJwtException").forward(request, response);
        } catch (SignatureException | AccessDeniedException e) {
            // 异常捕获,发送到signatureException
            request.setAttribute("signatureException", e);
            //将异常分发到/signatureException控制器
            request.getRequestDispatcher("/signatureException").forward(request, response);
        }
        super.doFilterInternal(request, response, chain);
    }

    /**
     * 这里从token中获取用户信息并新建一个token
     */
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        String token = tokenHeader.replace(TokenEnum.TOKEN_PREFIX.getValue(), "");

        String username = JwtTokenUtils.getUsername(token);
        String permission = JwtTokenUtils.getUserPermission(token);
        ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
        for (String p : Arrays.asList(permission.split(","))) {
            simpleGrantedAuthorities.add(new SimpleGrantedAuthority(p));
        }
        if (username != null) {
            return new UsernamePasswordAuthenticationToken(username, null, simpleGrantedAuthorities);
        }
        return null;
    }

    /**
     * token刷新
     *
     * @param tokenHeader
     */
    private void refreshToken(String tokenHeader) throws RefreshTokenException {
        String token = tokenHeader.replace(TokenEnum.TOKEN_PREFIX.getValue(), "");
        // 客户端token有没有过期
        boolean expiration = JwtTokenUtils.isExpiration(token);
        // 是否过期时间已将超出两倍
        if (expiration) {
            boolean twoTimesTokenExpiration = JwtTokenUtils.isTwoTimesTokenExpiration(token);
            // 没有,续期,否则抛出自定义异常
            if (!twoTimesTokenExpiration) {
                String username = JwtTokenUtils.getUsername(token);
                String permission = JwtTokenUtils.getUserPermission(token);
                JWTAuthorizationFilter.TOKEN.put(JwtTokenUtils.getUsername(token),JwtTokenUtils.createToken(username, permission, false));
            } else {
                throw new RefreshTokenException();
            }
        }
    }
}

此外在Login登录时要清除该登录用户的续命token,在loginService中添加以下代码

 // 重新登录清除相关用户token
        if(Objects.nonNull(JWTAuthorizationFilter.TOKEN.get(userName))){
            JWTAuthorizationFilter.TOKEN.put(userName,null);
        }

自定义异常代码

package com.hongseng.app.config.exception;

/**
 * @program: fire_control
 * @description:
 * @author: fbl
 * @create: 2021-01-20 08:37
 **/
public class RefreshTokenException extends RuntimeException {
}

全局异常代码捕获需要修改 RefreshTokenException 自定义异常也需要捕获

package com.hongseng.app.config.exception;

import enums.ErrorCodeEnum;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import result.Result;


/**
 * @program: fire_control
 * @description: 处理自定义的业务异常
 * @author: fbl
 * @create: 2021-01-15 16:21
 **/
@ControllerAdvice
public class GlobalExceptionHandler {

    /**
     * token过期
     *
     * @return
     */
    @ExceptionHandler(value = {ExpiredJwtException.class, RefreshTokenException.class})
    @ResponseBody
    public Result expiredJwtException() {
        return Result.failure(ErrorCodeEnum.SYS_ERR_TOKEN_EXPIRED);
    }


    /**
     * token错误
     *
     * @return
     */
    @ExceptionHandler(value = SignatureException.class)
    @ResponseBody
    public Result signatureException() {
        return Result.failure(ErrorCodeEnum.SYS_ERR_TOKEN_SIGNATURE);
    }

}

重定向异常发出controller也需要修改

package com.hongseng.app.controller;

import com.hongseng.app.config.exception.RefreshTokenException;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

/**
 * @program: fire_control
 * @description:
 * @author: fbl
 * @create: 2021-01-18 07:54
 **/
@RestController
public class JwtExceptionController {

    /**
     * 重新抛出异常
     */
    @RequestMapping("/expiredJwtException")
    public void expiredJwtException(HttpServletRequest request) throws ExpiredJwtException, RefreshTokenException {
        if (request.getAttribute("expiredJwtException") instanceof ExpiredJwtException) {
            throw ((ExpiredJwtException) request.getAttribute("expiredJwtException"));
        } else {
            throw new RefreshTokenException();
        }
    }

    @RequestMapping("/signatureException")
    public void signatureException(HttpServletRequest request) throws SignatureException {
        throw ((SignatureException) request.getAttribute("signatureException"));
    }


}

自身缓存的续命token,默认为null,需要续期时赋值,再次解析解析此token,此处使用ThreadLocal,为每一个线程分配一个token,防止并发修改

private static ThreadLocal<String> token = null;

token工具类

package utils;

import enums.TokenEnum;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;

import java.security.Key;
import java.util.Date;
import java.util.HashMap;

/**
 * @program: spring-security
 * @description:
 * @author: fbl
 * @create: 2020-12-02 14:08
 **/
public class JwtTokenUtils {
    // 创建token
    public static String createToken(String username, String permission, boolean isRememberMe) {
        byte[] keyBytes = Decoders.BASE64.decode(TokenEnum.SECRET.getValue());
        Key key = Keys.hmacShaKeyFor(keyBytes);

        long expiration = isRememberMe ? TokenEnum.EXPIRATION_REMEMBER.getTime() : TokenEnum.EXPIRATION.getTime();
        HashMap<String, Object> map = new HashMap<>();
        map.put(TokenEnum.ROLE_CLAIMS.getValue(), permission);
        return Jwts.builder()
                .signWith(key, SignatureAlgorithm.HS512)
                // 这里要早set一点,放到后面会覆盖别的字段
                .setClaims(map)
                .setIssuer(TokenEnum.ISS.getValue())
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
    }

    // 从token中获取用户名
    public static String getUsername(String token) {
        try {
            return  getTokenBody(token).getSubject();
        }catch (ExpiredJwtException e){
            return e.getClaims().getSubject();
        }

    }

   // 是否已过期
    public static boolean isExpiration(String token) {
        try {
            return getTokenBody(token).getExpiration().before(new Date());
        }catch (ExpiredJwtException e){
            return  e.getClaims().getExpiration().before(new Date());
        }
    }

    private static Claims getTokenBody(String token) {
        return Jwts.parser()
                .setSigningKey(TokenEnum.SECRET.getValue())
                .parseClaimsJws(token)
                .getBody();
    }

    public static String getUserPermission(String token) {
        try {
            return (String) getTokenBody(token).get(TokenEnum.ROLE_CLAIMS.getValue());
        }catch (ExpiredJwtException e){
            return (String) e.getClaims().get(TokenEnum.ROLE_CLAIMS.getValue());
        }

    }

    /**
     * token时间没有超过期时间的两倍,续期,否则重新登录
     *
     * @param token
     * @return
     */
    public static boolean isTwoTimesTokenExpiration(String token) {
       try {
            Claims tokenBody = getTokenBody(token);
            Date expiration = tokenBody.getExpiration();
            return expiration.getTime() + TokenEnum.EXPIRATION.getTime() * 1000 * 2   < System.currentTimeMillis();
        }catch (ExpiredJwtException e){
            long expirationTime = e.getClaims().getExpiration().getTime() + TokenEnum.EXPIRATION.getTime() * 1000 * 2  ;
            return expirationTime < System.currentTimeMillis();
        }
    }
}


在使用工具类的时候有一个小问题
如果token已经过期,我们拿着过期的token去使用工具类解析,会报ExpiredJwtException
但是我的需求就是获取到过期token的过期时间加上配置的过期毫秒数乘以2与当前时间做对比来判断要不要续命。此时尴尬的一笔

不过没得关系,我们try-catch,通过源码发现ExpiredJwtException 中有我们需要的信息

 public static boolean isTwoTimesTokenExpiration(String token) {
       try {
            Claims tokenBody = getTokenBody(token);
            Date expiration = tokenBody.getExpiration();
            return expiration.getTime() + TokenEnum.EXPIRATION.getTime() * 1000 * 2   < System.currentTimeMillis();
        }catch (ExpiredJwtException e){
            long expirationTime = e.getClaims().getExpiration().getTime() + TokenEnum.EXPIRATION.getTime() * 1000 * 2  ;
            return expirationTime < System.currentTimeMillis();
        }
    }

同样在jwt解析过期的token获取用户名,权限的时候也要如此,catch掉ExpiredJwtException

### 如何设置或延长 JWT Token过期时间 JWT(JSON Web Token)是一种广泛使用的身份验证机制,但由于其不可变性,一旦签发便无法修改或撤销。因此,处理 JWT 过期问题通常需要引入额外的技术手段。 #### 使用 Refresh Tokens 延长认证生命周期 Refresh Tokens 是一种常见的解决方案,用于在 Access Token 到期后重新获取新的有效令牌。这种方式通过分离短期有效的 Access Token 和长期有效的 Refresh Token 来增强安全性[^1]。Access Token 负责日常的身份验证请求,而 Refresh Token 仅用于安全环境下换取新令牌。 #### Redis 缓存 Token 实现动态管理 另一种方式是利用 Redis 缓存来管理和延长 JWT过期时间。Redis 提供高效的键值存储功能,可以通过设定 TTL(Time To Live)属性控制每个 Token 的有效期。每当用户携带合法的 Token 访问服务端时,都可以触发逻辑更新该 Token 对应的 Redis 键的有效期限[^3]。 以下是基于 Redis 动态调整 JWT 生命周期的一个简单实现: ```python import redis from datetime import timedelta, datetime # 初始化 Redis 客户端 r = redis.Redis(host='localhost', port=6379, db=0) def update_token_expiration(token: str): """ 更新指定 TokenRedis 中的过期时间 """ key = f'token:{token}' if r.exists(key): # 如果存在,则更新过期时间 new_expiry_time = datetime.now() + timedelta(minutes=30) r.expireat(key, int(new_expiry_time.timestamp())) return True return False def authenticate_and_update(user_id: str, password: str) -> str: """ 验证用户名密码并生成/更新 Token """ if verify_credentials(user_id, password): # 自定义函数验证凭证 token = generate_jwt(user_id) # 自定义函数生成 JWT r.set(f'token:{token}', user_id, ex=timedelta(minutes=30)) return token raise ValueError('Invalid credentials') def check_token_validity(token: str) -> bool: """ 检查 Token 是否仍然有效 """ return r.exists(f'token:{token}') ``` 上述代码展示了如何结合 Redis 存储和操作 Token 数据,并允许每次成功调用 API 后刷新 Token 的存活周期[^4]。 #### 不依赖外部库的手动续约方案 如果不想借助第三方工具或者框架支持自动续期的功能,也可以手动编写类似的续约流程。例如,在 Java 应用程序中,可以在每次接收到客户端发送过来的有效 Token 请求之后立即重建一个新的带有更晚截止日期的新实例返回给前端应用继续使用直到再次接近结束为止[^5]。 总之,无论是采用 refresh tokens 方法还是依靠内存数据库像 Redis 来辅助完成这项工作都各有优劣需视具体场景需求决定最佳实践路径。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值