20241219 TIL : Refresh Token with Spring Boot

MCS·2024년 12월 19일

TIL

목록 보기
28/45

오늘 학습한 내용

  • Refresh Token with Spring Boot
    • Redis 실행
    • Redis 관련 설정파일 작성
    • Refresh Token 발급하기

Refresh Token with Spring Boot

Redis 실행

도커를 이용해 Redis를 설치해 보자.
docker run -p 6379:6379 -d redis 을 터미널에서 실행한다.

Redis 관련 설정파일 작성

여기서부터는 Spring Boot에서 진행한다.

application.yml

spring:	
	data:
    redis:
      host: ${REDIS_HOST}
      port: ${REDIS_PORT}
      password: ${REDIS_PW}

jwt:
  secret-key: ${SECRET_KEY}
  expiredMillis: 600

레디스와 jwt 관련 설정을 추가해준다.

RedisConfig

@Configuration
@EnableRedisRepositories
public class RedisConfig {
    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Value("spring.data.redis.password")
    private String redisPassword;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
        config.setPassword(redisPassword);
        return new LettuceConnectionFactory(config);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        // 일반적인 key:value의 경우 시리얼라이저
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        // Hash를 사용할 경우 시리얼라이저
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new StringRedisSerializer());

        // 모든 경우
        redisTemplate.setDefaultSerializer(new StringRedisSerializer());

        return redisTemplate;
    }
}

RedisRepository를 사용하기 위해 @EnableRedisRepositories 어노테이션을 설정해주고, RedisTemplate을 사용해 직렬화 설정을 추가한다.

Refresh Token 발급하기

token entity

@RedisHash("token")
@AllArgsConstructor
@Getter
public class Token {
    @Id
    private Long id;
    private String refreshToken;
    @TimeToLive
    private Long expiration;
}

레디스에 저장할 refresh token 엔티티를 작성한다. id는 RedisHash와 함께 key 값이 되고 value는 refreshToken 이다. 토큰을 생성할 때 레디스에 저장할 것이고, @TimeToLive 어노테이션을 통해 만료 기간을 정해준다.

@RedisHash(value = "token", timeToLive = 10)와 같이 TTL을 설정해 줄 수도 있다.

TokenRepository

@Repository
public interface TokenRepository extends CrudRepository<Token, Long> {
}

Redis에 토큰을 저장하기 위한 Repository이다.

JwtUtil

@Component
@RequiredArgsConstructor
public class JwtUtil {
    @Value("${jwt.secret-key}")
    private String secret;
    private int expirationTimeMillis = 864_000_000; // 10일(밀리 초 단위)
    @Value("${jwt.refresh-token-expiration-mills}")
    private int refreshTokenExpirationMillis;
    private String tokenPrefix = "Bearer ";
    private ObjectMapper objectMapper = new ObjectMapper();
    private final TokenRepository tokenRepository;

		// ...

		public String createRefreshToken(Long id, Instant issuedAt) {
        String refreshToken = JWT.create()
                .withClaim("id", id)
                .withIssuedAt(issuedAt)
                .withExpiresAt(issuedAt.plusMillis(refreshTokenExpirationMillis))
                .sign(Algorithm.HMAC512(secret));

        Token token = new Token(id, refreshToken);
        tokenRepository.save(token);
        return refreshToken;
    }

JwtUtil에 refresh token을 발급하는 로직을 추가한다.

Controller

@RestController
@RequiredArgsConstructor
public class TokenController {
    private final JwtUtil jwtUtil;

    @GetMapping("/refresh/{id}")
    public ResponseEntity<RefreshTokenResponse> getRefresh(@PathVariable("id") Long id) {
        String refreshToken = jwtUtil.createRefreshToken(id, Instant.now());
        RefreshTokenResponse response = new RefreshTokenResponse(refreshToken);
        return ResponseEntity.ok(response);
    }
}

컨트롤러에 토큰을 발급하는 API를 추가한다.
아래 dto를 응답 body로 사용한다.

@Getter
@AllArgsConstructor
public class RefreshTokenResponse {
    private String refreshToken;
}

이제 /refresh/{id} 경로로 요청을 보내면, refresh token을 발급한다.

위의 코드는 refresh token을 발급하는 예시이고, 실제로는 더 복잡한 과정이 필요하다.

@PostMapping("/refresh")
    public ResponseEntity<?> refreshAccessToken(@RequestBody RefreshTokenRequest request) {
        String refreshToken = request.getRefreshToken();

        // 1. Refresh Token 검증
        if (!jwtUtil.validateToken(refreshToken)) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid or expired refresh token");
        }

        // 2. Refresh Token에서 사용자 정보 추출
        Long userId = jwtUtil.extractUserId(refreshToken);

        // 3. 새로운 Access Token 생성
        String newAccessToken = jwtUtil.createAccessToken(userId, Instant.now());

        // 4. 필요한 경우 새로운 Refresh Token 생성 (옵션)
        String newRefreshToken = jwtUtil.createRefreshToken(userId, Instant.now());

        // 5. 응답 반환
        RefreshTokenResponse response = new RefreshTokenResponse(newAccessToken, newRefreshToken);
        return ResponseEntity.ok(response);
    }

이 예시 코드처럼 refresh token으로 access token을 발급하는 로직도 필요할 것이다. 필요한 로직은 Service 단으로 분리해서 사용해야 한다.

이렇게 발급 시 refresh token과 access token을 함께 발급하고, 기존의 refresh token을 비활성화 시키는 로직도 있어야 할 것이다.

프론트엔드에서는 refresh token을 HttpOnly 쿠키에 저장하고, access token이 만료되면 refresh token을 통해 access token을 재발급하도록 하며, 둘 다 만료시 다시 로그인하도록 만든다.
로그아웃 처리 시 refresh token을 redis에서 삭제한다.

이제 refresh token과 RTR 방식을 사용한 토큰 발급을 할 수 있게 되었다.

profile
백엔드를 잘 하고 싶은 사람

0개의 댓글