思路:

由于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