카카오 로그인 구현하기(3) - Redis + 액세스 토큰 재발급하기

1. Refresh 토큰 저장 방식

 

Refresh Token은 Access Token을 재발급하기 위한 용도

 

Refresh Token을 쿠키에 저장하면 오히려 보안성만 떨어뜨리는 행위가 됩니다. 쿠키는 CSRF 공격에 취약하다는 점을 가지고 있어 좋지 않은 방법이라고 결론을 내렸습니다. 마찬가지로 Refresh Token을 세션 스토리지에 저장하는 것도 XSS 공격의 취약성을 가지고 있습니다. 따라서 Refresh Token을 Redis에 저장하는 방식을 채택했습니다.
출처 : https://byungil.tistory.com/309

 

1.1 Redis 저장 방식의 장점

 

  1. Key - Value 방식, 인메모리 DB 방식으로 빠르게 접근할 수 있음.
  2. 브라우저에 비해 탈취 가능성이 낮다고 생각하는 redis 서버에 저장하는 방식
  3. Refresh Token은 영구적으로 저장되는 데이터가 아님. Refresh Token은 영구적으로 저장될 필요가 없기 때문에 In-Memory DB를 사용해도 충분하며, 성능 이점을 챙길 수 있음

https://www.notion.so/SpringBoot-Redis-a54ce125a0494eb79cbe4289505564db?pvs=21

 

 

1.2 로직

  1. Token 재발급 API를 구현
  2. Frontend에서 Access Token 만료 응답을 받게 될 경우, Request Header에 Refresh Token을 담아 Token 재발급 API로 재발급 요청을 보냄
  3. DB로부터 해당 사용자의 Refresh Token을 조회하여 Request Header의 Refresh Token과 비교
  4. 일치할 경우 Token 재발급 후 DB의 Refresh Token을 업데이트하고, Frontend 측에게 Response로 전달

 

2. RefreshToken을 이용하여 AccessToken 갱신하기

 

2.1 RefreshToken Entity 작성

@Builder
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@RedisHash(value = "refreshToken", timeToLive = 14440)  // 4 hours
public class RefreshToken {

    @Id
    private String id;

    String token;

}

 

@Id 어노테이션

 

  • java.persistence.id가 아닌 opg.springframework.data.annotation.Id 를 import 해야 함.
  • Refresh Token은 Redis에 저장하기 때문에 JPA 의존성이 필요하지 않음

 

@RedisHash 어노테이션

 

  • Redis Lettuce를 사용하기 위해 작성해야 됨

 

value는 redis key 값으로 사용됨. redis 저장소의 key로는 {value}:{@Id 어노테이션을 붙여준 값}이 저장

 

2.2 RefreshToken Repository 작성

 

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, Long> {
}

 

Redis Template 방식이 아닌 Redis Repository 방식을 사용, JpaRepository가 아닌 CrudRepository를 상속받아야 함

 

  • CrudRepository를 상속받는 RedisRepository 방식을 이용
  • 별도의 Configuration 의존성 추가가 필요하지 않고 Redis Template 방식보다 훨씬 구현이 간편하기 때문

 

Redis는 전통적인 관계형 데이터베이스(RDBMS)가 아니라, 인메모리 데이터 저장소입니다. Redis는 주로 빠른 읽기/쓰기를 위해 사용되며, 관계형 데이터베이스에서 제공하는 고급 쿼리 기능, 페이징, 정렬 등의 기능은 제공하지 않습니다.

JpaRepository는 JPA (Java Persistence API) 기반의 리포지토리로, 관계형 데이터베이스를 대상으로 하는 다양한 기능을 제공합니다. 그러나 Redis는 JPA와 관련이 없기 때문에, JpaRepository에서 제공하는 기능이 Redis와는 맞지 않습니다.
-chatGPT-

 

2.3 RefreshToken Service 작성

@Service
@RequiredArgsConstructor
public class RefreshTokenService {

    private final RefreshTokenRepository refreshTokenRepository;
    private final MemberRepository memberRepository;
    private final JwtTokenProvider jwtTokenProvider;

    @Transactional
    public void saveRefreshToken(String refreshToken) {
        RefreshToken token = RefreshToken.builder().id(jwtTokenProvider.getMemberId(refreshToken)).token(refreshToken).build();
        refreshTokenRepository.save(token);
    }

    @Transactional
    public AuthToken reissueToken(String  refreshToken) {
        jwtTokenProvider.validateToken(refreshToken); // refresh token 유효성 검사

        RefreshToken oldRefreshToken = refreshTokenRepository.findById(jwtTokenProvider.getMemberId(refreshToken)).orElseThrow(() -> new GlobalException(GlobalErrorCode.AUTH_INVALID_REFRESH_TOKEN));

        if (!oldRefreshToken.getToken().equals(refreshToken)) { // 저장된 refresh token과 요청된 refresh token이 다를 경우
            throw new GlobalException(GlobalErrorCode.AUTH_INVALID_REFRESH_TOKEN);
        }

        String newAccessToken = jwtTokenProvider.generateAccessToken(oldRefreshToken.getId());
        String newRefreshToken = jwtTokenProvider.generateRefreshToken(oldRefreshToken.getId());

        saveRefreshToken(newRefreshToken);

        return AuthResponseDTO.AuthToken.builder().accessToken(newAccessToken).refreshToken(newRefreshToken).build();
    }
}

 

RefreshToken을 Redis에 저장하는 메서드와 RefreshToken을 통해 AccessToken을 재발행해주는 메서드

 

2.4 AuthService 및 AuthController 작성

 

@Override
public AuthToken reissueToken(String refreshToken) {
    return refreshTokenService.reissueToken(refreshToken);
}

 

AuthServiceImpl 클래스에 위 메서드를 추가

 


@Operation(summary = "토큰 재발급", description = "access 토큰이 만료된 경우 refresh 토큰을 통해 토큰을 재발급 합니다.")
@PostMapping("/kakao/reissue")
public ApiResponse<AuthResponseDTO.AuthToken> reissueToken(@GetToken String refreshToken) {
    return ApiResponse.onSuccess("토큰 재발급 성공", authService.reissueToken(refreshToken));
}

 

AuthController의 /kakao/reissue API