json web token(jwt)的超时刷新策略

9 篇文章 1 订阅
6 篇文章 0 订阅

1. 前言

我目前的项目是前后端分离的项目,前后端分离时我们会因为同源策略而无法设置cookie和sessionid,自然就用到了JWT(Json Web Token)来解决身份认证,相对于其他方式有很多难以媲美的优点。
但是呢,每一个技术都不是完美的,有优点则必有缺点,我们学习每一项技术的时候都要了解随之而来的不足!token实际上是一个拥有完整身份信息和过期时间的加密字符串,一经生成,就不可控。

2. 需求场景

  • 如何刷新token但同时有效的只有一个?
  • 如何在超时后自动刷新,而不是直接跳转登录页影响用户体验?

3.解决策略

  1. jwt的vo包装类
package com.smart.gateway.vo;

import lombok.Builder;
import lombok.Getter;
import java.io.Serializable;

@Builder
@Getter
public class JwtVo implements Serializable {
    //token令牌
    private String jwt;

    //刷新token,过期时间比jwt的长一点,用于判断用户是否活跃,并动态刷新token
    private String refreshJwt;

    //jwt的过期时间
    private Long expireTime;

}

简要说明:

  • jwt:是正常的token
  • refreshJwt:和jwt唯一不同的就是过期时间会延后点,根据实际情况进行调整,时间的差值即为判断用户是否活跃的一个阈值
  • expireTime:jwt的过期时间,可减少jwt的解析时间,减小服务器性能消耗,提高效率

由于项目使用了springsecurity来进行鉴权,因此使用了spring security的全局过滤器

package com.smart.gateway.filter;

import com.alibaba.fastjson.JSON;
import com.smart.gateway.service.ImmunityPathService;
import com.smart.pojo.Result;
import com.smart.pojo.StatusCode;
import com.smart.util.JwtUtil;
import com.smart.vo.JwtVo;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import org.springframework.util.Base64Utils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * description:
 * creator: 蝠鲼
 * create_time: 2020/7/22
 * version: v1.0
 */
@Component
@Slf4j
public class AuthorizeFilter implements GlobalFilter, Ordered {

    public static final String API_URI = "/v2/api-docs";

    public static final String LOGIN_URl = "/login";

    @Autowired
    private JwtUtil jwtUtil;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private ImmunityPathService immunityPathService;
    @Value("${jwt.tokenSign}")
    private String tokenSign;
    @Value("${token.header}")
    private String header;

    @Value("${doc.visualization.enable}")
    private Boolean visualization;

    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpResponse response = exchange.getResponse();
        String url = request.getURI().getPath();
        List<String> immuPaths = immunityPathConfig.getPaths();
        if (immuPaths.contains(url)){
            return chain.filter(exchange);
        }
       if (visualization && (url.indexOf(API_URI) >= 0)){
            return chain.filter(exchange);
        }
        String authorization = request.getHeaders().getFirst(tokenSign);
        //没有特定的请求头以及请求头不是以特定字符串开头的直接返回
        if (StringUtils.isBlank(authorization)){
            Result result = Result.failure(StatusCode.OPERATION_ERROR_WITHOUT_TOKEN);
            byte[] bits = JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = response.bufferFactory().wrap(bits);
            response.getHeaders().add("Content-Type", "application/json; charset= UTF-8");
            return response.writeWith(Mono.just(buffer));
        }
        //判断字符串头部
        if (!authorization.startsWith(header)){
            Result result = Result.failure(StatusCode.OPERATION_ERROR_TOKEN);
            byte[] bits = JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = response.bufferFactory().wrap(bits);
            response.getHeaders().add("Content-Type", "application/json; charset= UTF-8");
            return response.writeWith(Mono.just(buffer));
        }
        //将字符串base64解密
        String jwtVoStr = authorization.substring(7);
        byte[] jwtVoStrByte = Base64Utils.decodeFromString(jwtVoStr);
        JwtVo jwtVo = JSON.parseObject(new String(jwtVoStrByte), JwtVo.class);
        //判断jwt是否为空
        if (StringUtils.isBlank(jwtVo.getJwt())){
            Result result = Result.failure(StatusCode.OPERATION_ERROR_WITHOUT_TOKEN);
            byte[] bits = JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8);
            DataBuffer buffer = response.bufferFactory().wrap(bits);
            response.getHeaders().add("Content-Type", "application/json; charset= UTF-8");
            return response.writeWith(Mono.just(buffer));
        }
        //判断jwt是否过期
        //先通过expireTime快速判断第一遍
        //若过期
        Date date = new Date();
        if (date.after(new Date(jwtVo.getExpireTime()))){
            //再进入token判断一下,防止篡改
            //过期则继续后续判断
            if (jwtUtil.isTokenExpired(jwtVo.getJwt())){
                //判断refreshjwt是否为空
                if (StringUtils.isBlank(jwtVo.getRefreshJwt())){
                    Result result = Result.failure(StatusCode.OPERATION_ERROR_WITHOUT_TOKEN);
                    byte[] bits = JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8);
                    DataBuffer buffer = response.bufferFactory().wrap(bits);
                    response.getHeaders().add("Content-Type", "application/json; charset= UTF-8");
                    return response.writeWith(Mono.just(buffer));
                }
                //再判断refreshJwt是否也过期
                //若refreshjwt过期,直接重定向到登录页
                if (jwtUtil.isTokenExpired(jwtVo.getRefreshJwt())){
                    //303状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源
                    response.setStatusCode(HttpStatus.SEE_OTHER);
                    response.getHeaders().set(HttpHeaders.LOCATION, LOGIN_URl);
                    return response.setComplete();
                }
                //若refreshjwt没过期,需要刷新jwt并返回给前端
                JwtVo jv = jwtUtil.refreshToken(jwtVo.getJwt());
                byte[] bytes = JSON.toJSONString(jv).getBytes();
                String enStr = Base64Utils.encodeToUrlSafeString(bytes);
                Map<String, String> map = new HashMap<>();
                map.put("token",enStr);
                Result result = Result.failure(StatusCode.TOKEN_NEEDS_TO_BE_REFRESHED,map);
                byte[] bits = JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8);
                DataBuffer buffer = response.bufferFactory().wrap(bits);
                response.getHeaders().add("Content-Type", "application/json; charset= UTF-8");
                return response.writeWith(Mono.just(buffer));
            }
        }
        //未过期直接放行
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}

简化一下逻辑如下:

  1. 从请求头中取出自定义的鉴权包装token
  2. 包装token采用base64加密,因此需要再解密
  3. 先判断jwt是否过期,未过期直接放行
  4. 在判断refresh是否过期,也过期的话直接重定向到登录页
  5. jwt过期但refreshJwt未过期,这里直接生成新的包装token,base64加密后返回

4.总结

巧妙的用了一个jwt的包装类,实际上就是第二个refreshJwt只是比jwt过期时间长些,且只调用一次,它被调用的时候必然jwt已经过期了,但用户还属于在活跃区间,这时后台刷新token,用户完全是没有感觉的,并且刷新token是在jwt过期之后,也不存在同一个用户同时存在多个有效token存在的问题!可以说是兼顾了用户体验和安全性

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值