🍀 简窝 Blog
📃 文章详情

Token refresh的实现

实现原理:

和access_token一样, 在request请求里加入refresh_token标识,给access_token设置短时间的期限(例如2小时),给refresh_token设置长时间的期限(例如七天)。当活动用户(拥有access_token)发起request时,在权限验证里,对于requeset的header包含的access_token、refresh_token分别进行验证:

1、 access_token过期, 权限认证失败, 用户跳转到登录页面
2、access_token没过期, 即通过权限验证
  a. 当access_token签发时间 < 当前时间 < (签发时间+((token过期时间-token签发时间) / 3 * 2)), 不刷新token
  b. 当(签发时间+((token过期时间-token签发时间) / 3 * 2)) < 当前时间 < token过期时间, 刷新token并返回给前端, 再在客户端缓存里更新access_token值
3、access_token过期, refresh_token过期即权限验证失败, 用户跳转到登录页面

 

下面展示一下关键代码:

一、登录生成token的时候加入refresh标识

 

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("==>>LoginFilter.successfulAuthentication start...");

        AuthUserTokenBO user = (AuthUserTokenBO) authResult.getPrincipal();
        //将userId存入session(程序不再使用session)
//        request.getSession().setAttribute(Constants.CURRENT_USER, user.getAuthToken().getUid());

        generateAccessToken(response, user);
        generateRefreshToken(response, user);

        String encryptStr = JwUtil.encrypt(String.valueOf(user.getAuthToken().getUid()));
        response.addHeader(Constants.LOGIN_ID_SECRET, encryptStr);
        response.addHeader(Constants.LOGIN_ID, user.getUsername());
        response.getWriter().write(processSuccessMsg(Constants.SUCCESS_LOGIN));
        LoginLogEvent event = new LoginLogEvent(this, user.getUsername(), request);
        event.setEventTypeEnum(LoginEventTypeEnum.LOGIN);
        event.setIsSuccess(true);//
        applicationContext.publishEvent(event);
        try {
            loginFailedBizService.doVoidRecord(user.getUsername(), JwUtil.getRealIpAddress(request));
        } catch (JwBlogException e) {
            log.error(">>LoginFilter.successfulAuthentication exec failed, e:\r\n", e);

        }
        log.info("==>>LoginFilter.successfulAuthentication end...");

    }

二、在权限验证环节,对于access_token、refresh_token设置不同的时间期限, 计算是否要刷新access_token, 再根据判断结果返回给前端。

 private JwAuthenticationToken getAuthentication(HttpServletRequest request, HttpServletResponse response,
                                                    UserBO userBO) {
        log.info("==>>JwtAuthenticationFilter.getAuthentication start...");
        List<GrantedAuthority> authorities = new ArrayList<>();
        String accessToken = request.getHeader(Constants.ACCESS_TOKEN);
        if (StringUtils.isBlank(accessToken)) {
            accessToken = request.getParameter(Constants.ACCESS_TOKEN);
        }
        String refreshToken = request.getHeader(Constants.ACCESS_TOKEN);
        if (StringUtils.isBlank(refreshToken)) {
            refreshToken = request.getParameter(Constants.ACCESS_TOKEN);
        }
        log.info("==>>JwtAuthenticationFilter.getAuthentication access token {}...", accessToken);
        log.info("==>>JwtAuthenticationFilter.getAuthentication refresh token {}...", refreshToken);

        if (StringUtils.isNotBlank(accessToken)) {
            Long oid = null;
            try {
                Claims claims = JwtUtils.parse(accessToken);
                Long refresgOid = null;
                Date refreshExpiredDate = null;
                if (StringUtils.isNotBlank(refreshToken)) {
                    Claims refreshClaims = JwtUtils.parse(refreshToken);
                    refresgOid = Long.parseLong(String.valueOf(refreshClaims.get(USER_KEY)));
                    refreshExpiredDate = refreshClaims.getExpiration();

                }
                oid = Long.parseLong(String.valueOf(claims.get(USER_KEY)));
                request.setAttribute(USER_KEY, oid);

                String loginIdCacheKey = MessageFormat.format(CacheKeyConstants.LOGIN_USER_STATUS, oid);
                boolean isLogin = (Boolean) jwCacheStore.get(loginIdCacheKey).orElse(false);
                // 解决服务器每次重启缓存失效,但是jwt有效的问题,调试时可以注释
                if (!Boolean.TRUE.equals(isLogin)) {
                    log.error("======>>user logout failed, user does not login.");
                    return null;
                }
                // 验证是否失效
                String invalidTokenKey = MessageFormat.format(CacheKeyConstants.INVALID_TOKEN, accessToken);
                if (jwCacheStore.hasKey(invalidTokenKey)) {
                    return null;
                }

                // token签发时间
                long issuedAt = claims.getIssuedAt().getTime();
                // 当前时间
                long currentTimeMillis = System.currentTimeMillis();
                // token过期时间
                long expirationTime = claims.getExpiration().getTime();

                // 1. 签发时间 < 当前时间 < (签发时间+((token过期时间-token签发时间) / 3 * 2)) 不刷新token
                // 2. (签发时间+((token过期时间-token签发时间) / 3 * 2)) < 当前时间 < token过期时间 刷新token并返回给前端
                // 3. tokne过期时间 < 当前时间 跳转登录,重新登录获取token
                // 验证token时间有效性
                if (!isReceiveMsgNotofy(request)) {
                    if (Objects.equals(oid, refresgOid) && refreshExpiredDate.getTime() > currentTimeMillis) {
                        if ((issuedAt + ((expirationTime - issuedAt) / 3 * 2)) < currentTimeMillis
                                && currentTimeMillis < expirationTime) {
                            log.info("JWT token (url=[{}]) will expire  in {} Millis, system will refreshToken.",
                                    request.getServletPath(), (expirationTime - currentTimeMillis));
                            String refreshAccessToken = JwtUtils.sign(claims, Long.parseLong(accessTokenExpireSec) * 1000);
                            // 将token放入响应头中
                            response.setContentType(Constants.CONTENT_TYPE_JSON);
                            response.addHeader(Constants.REFRESH_ACCESS_TOKEN, refreshAccessToken);
                        }
                    }
                }


            } catch (Exception e) {
                log.info("==>>JwtAuthenticationFilter.getAuthentication exec failed, e\r\n", e);
                return null;
            }
            // 从数据库中取出用户信息
            AdminBizService adminBizService = SpringUtil.getBean(AdminBizService.class);
            try {
                AdminBO admin = adminBizService.queryAdminByOid(oid);
                // 实际上不会为null,直接抛出异常
                if (admin == null) {
                    return null;
                }
                userBO.setName(admin.getUsername());
                Map param = new HashMap();
                param.put(Constants.LOGIN_IP, JwUtil.getRealIpAddress(request));

                authorities.add(
                        new SimpleGrantedAuthority(Constants.ROLE_PREFIX + Constants.ADMIN.toUpperCase(Locale.ROOT)));
                // 这里直接注入角色,因为JWT已经验证了用户合法性,所以principal和credentials直接为null即可
                return new JwAuthenticationToken(authorities, admin.getUsername(), null, param);
            } catch (JwBlogException e) {
                log.info("==>>JwtAuthenticationFilter.getAuthentication exec failed, e\r\n", e);
                return null;
            }
        }
        log.info("==>>JwtAuthenticationFilter.getAuthentication start...");
        return null;

    }

📑 目录