Стратегия обновления времени ожидания токена (с исходным кодом)

Java
Стратегия обновления времени ожидания токена (с исходным кодом)

сценарий спроса

Когда мы делаем пользовательский центр, мы выдаем соответствующий токен для вошедшего в систему пользователя и устанавливаем его фиксированный срок действия для токена. Однако мы обычно сталкиваемся с такой проблемой: в течение срока действия пользователи могут без проблем носить токен для доступа, когда срок действия истекает, токен недействителен, и пользователю необходимо снова войти в систему, чтобы получить новый токен.

Представьте себе следующее: если я установлю время токена на 15 минут в производственной среде, и пользователю нужно снова входить в систему каждые 15 минут, этот опыт может быть убит пользователем.

Цели

Отличный опыт должен заключаться в следующем: активные пользователи должны получить новый токен после истечения срока действия токена, не зная об этом, и носить этот новый токен для доступа, в то время как пользователи, которые не были активны в течение длительного времени, должны повторить попытку после истечения срока действия токена. Это так называемый механизм обновления токена.

Что касается базового понимания и использования JWT, я не буду здесь вдаваться в подробности.Если у вас есть какие-либо сомнения, вы можете сначала прочитать следующие две статьи:После прочтения этого Session, Cookie, Token и спора с интервьюером проблем нет

Боевая система Springboot+JWT (с исходным кодом)

В этой статье мы интегрируем Springboot, Redis и JWT, чтобы подробно объяснить механизм обновления токена.

Идеи дизайна

Когда пользователь входит в систему, для вошедшего в систему пользователя выдается JWT и устанавливается время действия JWT.В то же время JWT пользователя сохраняется в кеше Redis, а срок действия кеша Срок действия сохраненного в кэше JWT больше, чем срок действия самого JWT. в:Время кэширования - время действия JWT = время обновления токена

Когда пользователи делают запросы к интерфейсу, им необходимо нести токены.В это время при выполнении запросов к интерфейсу обычно бывают следующие состояния:

  1. Токен не передается: в это время непосредственно возвращаемый токен пуст, а разрешения недостаточно;

  2. Входящий токен ошибки: в настоящее время ошибка токена возвращается напрямую, а разрешения недостаточно;

  3. Обычный входящий токен: отпустите.

  4. Обычный токен, но с истекшим сроком действия: запросите, существует ли токен в Redis, обновите токен, если он существует в кеше, поместите обновленный токен обратно в Redis и используйте обновленный токен для запроса ресурсов; если он не существует в кеше, период обновления токена прошел, пользователю необходимо снова войти в систему.

Код

Этот адрес Github механизма обновления JWT:https://github.com/bailele1995/springboot-jjwt.gitВ этой статье Xiaomazai публикует только ключевые части кода, связанные с идеей дизайна, для объяснения.

1. Логин пользователя

public ResultDTO login(String name, String password) {
        Map<String, Object> tokenMap = new HashMap<>();
        String token = null;
        try {
            User user = userMapper.findByUsername(name);
            //查询用户登录的账号是否存在
            if(user == null){
                 return ResultDTO.failure(new ResultError(UserError.EMP_IS_NULL_EXIT));
            }else{
                //校验用户密码是否正确
                if(!user.getPassword().equals(password)){
                    return ResultDTO.failure(new ResultError(UserError.PASSWORD_OR_NAME_IS_ERROR));
                }else {
                    // 获取token,将 user id 、userName保存到 token 里面
                    token = JwtUtil.sign(user.getUsername(),user.getId(),user.getPassword());
                    //将用户token保存至缓存中,并设置有效时间
                    redisUtils.set("userToken-" + user.getId(), token, 2L * 60);
                }
            }
            //校验token是否为空
            if(StringUtils.isEmpty(token)){
                return ResultDTO.failure(new ResultError(UserError.PASSWORD_OR_NAME_IS_ERROR));
            }
            tokenMap.put("token", token);
            tokenMap.put("user",user);
        } catch (Exception e) {
            e.printStackTrace();
        }
        //将登录用户信息和token返回给前端
        return ResultDTO.success(tokenMap);
    }

В этой части проверяется пользователь для входа, создается токен, который сохраняется в Redis. Сгенерированный токен и информация о пользователе возвращаются во внешний интерфейс.

2. Перехватчик

public class AuthenticationInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisUtils redisUtils;

    @Autowired
    UserMapper userMapper;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // 从 http 请求头中取出 token
        String token = httpServletRequest.getHeader("token");
        // 如果不是映射到方法直接通过
        if(!(object instanceof HandlerMethod)){
            return true;
        }
        HandlerMethod handlerMethod=(HandlerMethod)object;
        Method method=handlerMethod.getMethod();

        PrintWriter out = null ;
        //检查有没有需要用户权限的注解
        if (method.isAnnotationPresent(TokenRequired.class)) {
            TokenRequired userLoginToken = method.getAnnotation(TokenRequired.class);
            if (userLoginToken.required()) {
                // 执行认证
                //1.如果token为空,返回token为空
                if (token == null) {
                    out = httpServletResponse.getWriter();
                    out.append(JSON.toJSONString(ResultDTO.failure(new ResultError(UserError.NONE_TOKEN))));
                    return false;
                }
                // 2.获取 token 中的 user id,校验token
                String userId;
                try {
                    userId = JWT.decode(token).getClaim("userId").asString();
                } catch (JWTDecodeException j) {
                    out = httpServletResponse.getWriter();
                    out.append(JSON.toJSONString(ResultDTO.failure(new ResultError(UserError.TOKEN_CHECK_ERROR))));
                    return false;
                }
                //3.查询token中登录用户数据库中是否存在
                User user = userMapper.findUserById(userId);
                if (user == null) {
                    out = httpServletResponse.getWriter();
                    out.append(JSON.toJSONString(ResultDTO.failure(new ResultError(UserError.EMP_IS_NULL_EXIT))));
                    return false;
                }
                // 验证 token
                try {
                    if (!JwtUtil.verity(token, user.getPassword())) {
                        //如果token检验失败,查询redis中是否存在,redis为空,用户重新登录
                        if (redisUtils.get("userToken-" + user.getId()) == null) {
                            out = httpServletResponse.getWriter();
                            out.append(JSON.toJSONString(ResultDTO.failure(new ResultError(UserError.TOKEN_IS_VERITYED))));
                            return false;
                        }
                        //如果redis不为空,刷新token
                        String reToken = JwtUtil.sign(user.getUsername(),user.getId(),user.getPassword());
                        //将刷新后的token保存在redis中
                        redisUtils.set("userToken-" + user.getId(), reToken, 2L * 60);
                        out = httpServletResponse.getWriter();
                        out.append(JSON.toJSONString(ResultDTO.failure(reToken,new ResultError(UserError.TOKEN_IS_EXPIRED))));
                        return false;
                    }
                } catch (Exception e) {
                    out = httpServletResponse.getWriter();
                    out.append(JSON.toJSONString(ResultDTO.failure(new ResultError(UserError.TOKEN_CHECK_ERROR))));
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }
    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

Алгоритм обработки бизнес-запроса перехватчика выглядит следующим образом:

  1. Определить, содержит ли ресурс запроса пользователя токен, если нет, разрешение недостаточно;

  2. Если токен переносится, идентификатор пользователя получается из токена, и база данных проверяется, существует ли пользователь; если он не существует, токен недействителен и не будет выпущен;

  3. Проверьте, не истек ли срок действия токена.Если это обычный токен и срок его действия не истек, он освобождается.

  4. Обычный токен, но с истекшим сроком действия: запросите, существует ли токен в Redis, обновите токен, если он существует в кеше, поместите обновленный токен обратно в Redis и используйте обновленный токен для запроса ресурсов; если он не существует в кеше, период обновления токена прошел, пользователю необходимо снова войти в систему.

Суммировать

Просмотрите базовый процесс бизнес-оценки этого механизма обновления JWT:

  1. Пользователь входит в систему, генерирует токен и записывает токен в кеш;
  2. Когда пользователь обращается к странице, интерфейс запрашивает соответствующий интерфейс, проходит через перехватчик, и перехватчик вынимает токен из заголовка http-запроса;
  3. Проверьте, пуст ли токен, если токен пуст, доступ невозможен, если токен не пуст, запросите информацию о пользователе и проверьте токен;
  4. Если токен не пуст и пользователь существует, то токен правильный и выполняется проверка срока действия токена;
  5. Срок действия токена истек. Проверьте, существует ли токен в кеше. Если он существует, обновите его, чтобы сгенерировать новый токен, верните его во внешний интерфейс и поместите новый токен в кеш. Если он не существует, пользователь необходимо снова войти в систему.

Наконец

Наконец, спасибо за чтение. Цель статьи — зафиксировать и поделиться, если в статье есть явные ошибки, пожалуйста, указывайте на них, и мы будем учиться вместе в ходе обсуждения. Большое тебе спасибо !

Если вы считаете, что эта статья полезна для вас, то поставьте лайк, ваша поддержка и поддержка являются для меня движущей силой двигаться вперед~

Добро пожаловать, чтобы обратить внимание на мой публичный аккаунт WeChat [маленький программист], давайте вместе обсудим код и жизнь.