JWT реализует аутентификацию входа и автоматическое обновление токена.

Java

Раньше я в основном отвечал за модуль управления пользователями в проекте. Модуль управления пользователями будет включать в себя процесс шифрования и аутентификации. Шифрование было представлено в предыдущей статье, вы можете прочитатьМодуль управления пользователями: как обеспечить безопасность данных пользователей. Сегодня я расскажу о техническом выборе и реализации функции аутентификации. Технически это не сложно и, конечно, не сложно, но это также своего рода упражнение для наивного цыпленка, который раньше не писал функцию аутентификации.

Технический отбор

Для реализации функции аутентификации легко представить JWT или сеанс, но в чем разница между ними? Преимущества и недостатки каждого? Кто должен выбрать? Три убийства

разница

Основное различие между методами на основе сеанса и методами на основе JWT заключается в том, где сохраняется состояние пользователя.сессия хранится на сервере, покаJWT сохраняется на стороне клиентаиз

Процесс сертификации

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

Преимущества и недостатки

  • JWT хранится на стороне клиента и не требует дополнительной работы в распределенной среде. Поскольку сессия хранится на стороне сервера, необходимо реализовать совместное использование данных на нескольких машинах в распределенной среде.
  • Как правило, сеанс необходимо аутентифицировать в сочетании с файлами cookie, поэтому браузер должен поддерживать файлы cookie, чтобы мобильный терминал не мог использовать схему аутентификации сеанса.
безопасность
  • Полезная нагрузка JWT закодирована в base64, поэтому вКонфиденциальные данные не могут храниться в JWT. Информация о сеансе хранится на стороне сервера, что относительно безопаснее.

image.png

Если конфиденциальная информация хранится в JWT, она может быть очень небезопасно декодирована.

представление
  • После кодирования JWT будет очень длинным, предельный размер куки обычно 4k, и кука может не вместиться, поэтому JWT обычно помещается в локальное хранилище. И каждый http-запрос пользователя в системе будет содержать JWT в заголовке, а заголовок HTTP-запроса может быть больше, чем тело. И sessionId — это просто очень короткая строка, поэтому HTTP-запрос с использованием JWT намного дороже, чем использование сеанса.
Одноразовый

Безгражданство является характеристикой JWT, но также вызывает эту проблему, JWT одноразовая. Если вы хотите изменить содержимое внутри, вы должны выпустить новый JWT

  • не может быть отброшен
    После выдачи JWT он остается действительным до истечения срока его действия и не может быть аннулирован на полпути. Если вы хотите отказаться от него, распространенный метод лечения — объединить redis
  • Продлить
    Если для управления сеансом используется JWT, в структуру обычно встроена традиционная схема обновления файлов cookie.Сессия действительна в течение 30 минут.Если есть доступ в течение 30 минут, срок действия обновляется до 30 минут. Точно так же, чтобы изменить действительное время JWT, необходимо выпустить новый JWT. Самый простой способ — обновлять JWT при каждом запросе, т. е. каждый HTTP-запрос возвращает новый JWT. Этот метод не только жестокий и неэлегантный, но также требует JWT-шифрования и дешифрования для каждого запроса, что приведет к проблемам с производительностью. Другой способ - установить время истечения срока действия для каждого JWT отдельно в Redis и обновлять время истечения срока действия JWT при каждом доступе.

Выберите JWT или сеанс

Я голосую за JWT.JWT имеет много недостатков, но в распределенной среде нет необходимости реализовывать дополнительный многомашинный обмен данными наподобие сессий, хотя многомашинный обмен данными seesion можно пропуститьлипкая сессия,совместное использование сеанса,репликация сеанса,постоянный сеанс,Terracotta реализует репликацию сеансовЕсть много зрелых решений для решения этой проблемы. Но JWT не требует дополнительной работы, разве не приятно использовать JWT? И одноразовый недостаток JWT можно комбинировать с Redis, чтобы компенсировать его. Чтобы дополнить сильные стороны друг друга, в реальном проекте выбор заключается в использовании JWT для аутентификации.

Реализация функции

Зависимости, необходимые для JWT

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.10.3</version>
</dependency>

Класс инструментов JWT

public class JWTUtil {
    private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);

    //私钥
    private static final String TOKEN_SECRET = "123456";

    /**
     * 生成token,自定义过期时间 毫秒
     *
     * @param userTokenDTO
     * @return
     */
    public static String generateToken(UserTokenDTO userTokenDTO) {
        try {
            // 私钥和加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            // 设置头部信息
            Map<String, Object> header = new HashMap<>(2);
            header.put("Type", "Jwt");
            header.put("alg", "HS256");

            return JWT.create()
                    .withHeader(header)
                    .withClaim("token", JSONObject.toJSONString(userTokenDTO))
                    //.withExpiresAt(date)
                    .sign(algorithm);
        } catch (Exception e) {
            logger.error("generate token occur error, error is:{}", e);
            return null;
        }
    }

    /**
     * 检验token是否正确
     *
     * @param token
     * @return
     */
    public static UserTokenDTO parseToken(String token) {
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        JWTVerifier verifier = JWT.require(algorithm).build();
        DecodedJWT jwt = verifier.verify(token);
        String tokenInfo = jwt.getClaim("token").asString();
        return JSON.parseObject(tokenInfo, UserTokenDTO.class);
    }
}

инструкция:

  • Сгенерированный токен не имеет срока действия, а время истечения срока действия токена управляется Redis.
  • UserTokenDTO не содержит конфиденциальной информации, например, поле пароля не будет отображаться в токене.

Класс инструментов Redis

public final class RedisServiceImpl implements RedisService {
    /**
     * 过期时长
     */
    private final Long DURATION = 1 * 24 * 60 * 60 * 1000L;

    @Resource
    private RedisTemplate redisTemplate;

    private ValueOperations<String, String> valueOperations;

    @PostConstruct
    public void init() {
        RedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setValueSerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        redisTemplate.setHashValueSerializer(redisSerializer);
        valueOperations = redisTemplate.opsForValue();
    }

    @Override
    public void set(String key, String value) {
        valueOperations.set(key, value, DURATION, TimeUnit.MILLISECONDS);
        log.info("key={}, value is: {} into redis cache", key, value);
    }

    @Override
    public String get(String key) {
        String redisValue = valueOperations.get(key);
        log.info("get from redis, value is: {}", redisValue);
        return redisValue;
    }

    @Override
    public boolean delete(String key) {
        boolean result = redisTemplate.delete(key);
        log.info("delete from redis, key is: {}", key);
        return result;
    }

    @Override
    public Long getExpireTime(String key) {
        return valueOperations.getOperations().getExpire(key);
    }
}

Простой пакет RedisTemplate

реализация бизнеса

Функция входа
public String login(LoginUserVO loginUserVO) {
    //1.判断用户名密码是否正确
    UserPO userPO = userMapper.getByUsername(loginUserVO.getUsername());
    if (userPO == null) {
        throw new UserException(ErrorCodeEnum.TNP1001001);
    }
    if (!loginUserVO.getPassword().equals(userPO.getPassword())) {
        throw new UserException(ErrorCodeEnum.TNP1001002);
    }

    //2.用户名密码正确生成token
    UserTokenDTO userTokenDTO = new UserTokenDTO();
    PropertiesUtil.copyProperties(userTokenDTO, loginUserVO);
    userTokenDTO.setId(userPO.getId());
    userTokenDTO.setGmtCreate(System.currentTimeMillis());
    String token = JWTUtil.generateToken(userTokenDTO);

    //3.存入token至redis
    redisService.set(userPO.getId(), token);
    return token;
}

инструкция:

  • Проверьте правильность имени пользователя и пароля
  • Если имя пользователя и пароль верны, токен будет сгенерирован
  • Сохраните сгенерированный токен в Redis
функция выхода из системы
public boolean loginOut(String id) {
     boolean result = redisService.delete(id);
     if (!redisService.delete(id)) {
        throw new UserException(ErrorCodeEnum.TNP1001003);
     }

     return result;
}

Удалить соответствующий ключ

Функция обновления пароля
public String updatePassword(UpdatePasswordUserVO updatePasswordUserVO) {
    //1.修改密码
    UserPO userPO = UserPO.builder().password(updatePasswordUserVO.getPassword())
            .id(updatePasswordUserVO.getId())
            .build();
    UserPO user = userMapper.getById(updatePasswordUserVO.getId());
    if (user == null) {
        throw new UserException(ErrorCodeEnum.TNP1001001);
    }

    if (userMapper.updatePassword(userPO) != 1) {
        throw new UserException(ErrorCodeEnum.TNP1001005);
    }
    //2.生成新的token
    UserTokenDTO userTokenDTO = UserTokenDTO.builder()
            .id(updatePasswordUserVO.getId())
            .username(user.getUsername())
            .gmtCreate(System.currentTimeMillis()).build();
    String token = JWTUtil.generateToken(userTokenDTO);
    //3.更新token
    redisService.set(user.getId(), token);
    return token;
}

инструкция:
При обновлении пароля пользователя необходимо повторно сгенерировать новый токен и вернуть новый токен на внешний интерфейс. Интерфейсный интерфейс обновляет токен, хранящийся в локальном хранилище, и обновляет токен, хранящийся в Redis. Это может избежать повторного входа пользователя в систему. , и пользовательский опыт не так уж плох

другие инструкции
  • В реальном проекте пользователи делятся на обычных пользователей и пользователей-администраторов, только пользователи-администраторы имеют право удалять пользователей, эта функция также включает операции с токенами, но мне лень писать демонстрационный проект.
  • В реальном проекте передача пароля зашифрована

класс перехватчика

public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
                             Object handler) throws Exception {
    String authToken = request.getHeader("Authorization");
    String token = authToken.substring("Bearer".length() + 1).trim();
    UserTokenDTO userTokenDTO = JWTUtil.parseToken(token);
    //1.判断请求是否有效
    if (redisService.get(userTokenDTO.getId()) == null 
            || !redisService.get(userTokenDTO.getId()).equals(token)) {
        return false;
    }

    //2.判断是否需要续期
    if (redisService.getExpireTime(userTokenDTO.getId()) < 1 * 60 * 30) {
        redisService.set(userTokenDTO.getId(), token);
        log.error("update token info, id is:{}, user info is:{}", userTokenDTO.getId(), token);
    }
    return true;
}

инструкция:
Перехватчик в основном делает две вещи: одна — проверяет токен, а другая — определяет, нужно ли обновлять токен.
проверка токена:

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

Автоматическое обновление токена: чтобы реже использовать redis, время истечения срока действия обновляется только тогда, когда до истечения срока действия остается всего 30 минут.

Класс конфигурации перехватчика

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authenticateInterceptor())
                .excludePathPatterns("/logout/**")
                .excludePathPatterns("/login/**")
                .addPathPatterns("/**");
    }

    @Bean
    public AuthenticateInterceptor authenticateInterceptor() {
        return new AuthenticateInterceptor();
    }
}

напиши в конце

Если есть ошибки, укажите на них

поставить лайкгулять пешком