RefreshToken

woonie·2023년 4월 21일
1

JWT는 한번 발급되면 만료 전 까지 삭제할 수 없다.

토큰 재발급의 경우 다양한 방식으로 구현될 수 있으며, 해당 포스팅에서 구현된 방식은 여러 방식 중 하나이다.

Refresh Token을 사용하는 이유

access token은 발급 후 서버에 저장되지 않고 해당 토큰으로 사용자 권한을 인증하는 특징이 있다.

만약 access token이 탈취되면 토큰이 만료되기 전까지 해당 토큰을 갖고 있다면 누구나 권한 인증이 가능하다는 문제점이 있어 이를 보완하기 위해 access token의 만료 기간을 짧게 주는 방법이 있다.

하지만 사용자 측면에서는 토큰이 만료될때 마다 다시 로그인을 하여 토큰을 발급 받아야하는 불편함이 발생한다.

이를 해소하기 위해 refresh token을 사용하는데, access token에 비해 보다 더 긴 유효 기간으로 발급되며 권한을 부여하는게 아닌 access token을 재발급 하기 위해서만 사용된다는 특징이 있다.

Refresh Token의 문제 및 보완 방안

refresh token에도 문제점은 있다. 바로 무상태라는 특징으로 인해 access token과 마찬가지로 탈취 당할 위험이 있다.
만약 refresh token을 탈취 당했다면 해당 refresh token을 통해 access token을 재발급 받을 수 있게 된다.

하여 최초 로그인 시 refresh token을 DB 또는 Redis에 저장을 하는데 유저의 정보나, 요청이 온 ip와 함께 저장하고 재발급 요청 시 저장된 유저의 정보나 ip를 비교하여 재발급 여부를 결정한다.

구현

  • refresh token을 Redis와 DB 모두 저장하는 로직이다.
  1. access token 생성
 /**
   * Access 토큰을 생성하여 반환
   * @param authentication
   * @return access token
   */
  public String generateAccessToken(Authentication authentication) {
    // user 구분을 위해 Claims에 User 고유값인 email값을 넣음(UserDetailsServiceImpl 에서 email로 세팅)
    Claims claims = Jwts.claims().setSubject(authentication.getName());
    log.info("authentication.getName() : {}", authentication.getName());

    Date now = new Date();
    Date expiresIn = new Date(now.getTime() + access_token_expire_time);

    return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(expiresIn)
            .signWith(SignatureAlgorithm.HS256, access_token_secret_key)
            .compact();
  }

  1. refresh token 생성
/**
   * Refresh 토큰을 생성하여 반환
   * @param authentication
   * @return refresh token
   */
  public String generateRefreshToken(Authentication authentication) {
    Claims claims = Jwts.claims().setSubject(authentication.getName());

    Date now = new Date();
    Date expiresIn = new Date(now.getTime() + refresh_token_expire_time);

    return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(expiresIn)
            .signWith(SignatureAlgorithm.HS256, refresh_token_secret_key)
            .compact();
  }
  1. 로그인 시 토큰 저장
  • 사용자의 email 과 password로 인증과정에서 사용하는 UsernamePasswordAuthenticationToken 객체를 생성하여 인증 처리를 한다.
  • 생성된 refresh token과 유저의 정보를 Redis와 DB에 저장
  • 보안상 refresh token이 만료 전 재로그인 시 refresh token도 update되도록 구현
Authentication authentication = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(
                            loginDto.getEmail(),
                            loginDto.getPassword()
                    )
            );

            String refresh_token = jwtTokenProvider.generateRefreshToken(authentication);

            TokenDto tokenDto = new TokenDto(
                    jwtTokenProvider.generateAccessToken(authentication),
                    refresh_token
            );

            // Redis에 저장 - 만료 시간 설정을 통해 자동 삭제 처리
            redisTemplate.opsForValue().set(
                    authentication.getName(),
                    refresh_token,
                    refresh_token_expire_time,
                    TimeUnit.MILLISECONDS
            );

            // DB에 Refresh토큰 있는지 확인
            Optional<RefreshToken> refreshToken = refreshTokenRepository.findByEmail(loginDto.getEmail());

            if(refreshToken.isPresent()) {
                refreshTokenRepository.save(refreshToken.get().updateToken(refresh_token));
            }else {
                RefreshToken newToken = new RefreshToken(refresh_token, loginDto.getEmail());
                refreshTokenRepository.save(newToken);
            }
  1. 토큰 만료
  • 토큰이 만료 되었다면 refresh token으로 access token 재발급 요청
 /**
   * Refresh 토큰을 검증
   * @param token
   * @return
   */
  public boolean validateRefreshToken(String token) {
    try {
      Jwts.parser().setSigningKey(refresh_token_secret_key).parseClaimsJws(token);
      return true;
    } catch (JwtException e) {
      // MalformedJwtException | ExpiredJwtException | IllegalArgumentException
      throw new CustomException("Error on Refresh Token", HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

    // Refresh Token 검증
            if (!jwtTokenProvider.validateRefreshToken(refresh_token)) {
                throw new CustomException("Invalid refresh token supplied", HttpStatus.BAD_REQUEST);
            }

            // Refresh Token 에서 username 가져온다.
            Authentication authentication = jwtTokenProvider.getAuthenticationByRefreshToken(refresh_token);

            // Redis에서 저장된 Refresh Token 값을 가져온다.
            String redisSaveRefreshToken = redisTemplate.opsForValue().get(authentication.getName());
            if (!jwtTokenProvider.validateRefreshToken(redisSaveRefreshToken)) {
                throw new CustomException("Invalid refresh token supplied", HttpStatus.BAD_REQUEST);
            }
            // 토큰 재발행
            String new_refresh_token = jwtTokenProvider.generateRefreshToken(authentication);
            TokenDto tokenDto = new TokenDto(
                    jwtTokenProvider.generateAccessToken(authentication),
                    new_refresh_token
            );

            // RefreshToken Redis에 업데이트
            redisTemplate.opsForValue().set(
                    authentication.getName(),
                    new_refresh_token,
                    refresh_token_expire_time,
                    TimeUnit.MILLISECONDS
            );

            // DB에 저장된 Refresh Token 값을 가져온다.
            RefreshToken findRefreshToken = refreshTokenRepository.findByEmail(authentication.getName())
                    .orElseThrow(() -> new IllegalArgumentException("해당 RefreshToken은 없습니다."));
            System.out.println(findRefreshToken.getId());

            if (!jwtTokenProvider.validateRefreshToken(findRefreshToken.getRefreshToken())) {
                throw new CustomException("Invalid refresh token supplied", HttpStatus.BAD_REQUEST);
            }
            refreshTokenRepository.save(findRefreshToken.updateToken(new_refresh_token));
            
      
profile
동료들과 함께하는 개발의 중요성에 관심이 많습니다. 언제나 호기심을 갖고 꾸준히 노력하는 개발자로서 성장하고 있습니다.

0개의 댓글