Redis를 이용한 로그아웃

2025. 1. 14. 19:37TIL

 

블랙리스트를 이용하여 서버에서 로그아웃 기능을 수행하도록 설계할 것이다.

 

Redis DB 생성

 

의존성 추가

더보기
dependencies {

    implementation("org.springframework.boot:spring-boot-starter-data-redis")
}

 

Redis Config 설정

더보기
@Configuration
class RedisConfig {

    @Bean
    fun redisTemplate(connectionFactory: RedisConnectionFactory): RedisTemplate<String, String> {
        val redisTemplate = RedisTemplate<String, String>()
        redisTemplate.connectionFactory = connectionFactory
        redisTemplate.keySerializer = StringRedisSerializer()
        redisTemplate.valueSerializer = StringRedisSerializer()
        return redisTemplate
    }
}

 

토큰을 블랙리스트로 등록하고 블랙리스트인지 확인하는 Repository 작성

더보기
package hjp.hjchat.infra.redis

import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Repository
import java.util.concurrent.TimeUnit

@Repository
class TokenBlacklistRepository(
    private val redisTemplate: RedisTemplate<String, String>
) {
    fun addToBlacklist(token: String, expirationTime: Long) {
        redisTemplate.opsForValue().set(token, "blacklisted", expirationTime, TimeUnit.MILLISECONDS)
    }

    fun isBlacklisted(token: String): Boolean {
        return redisTemplate.hasKey(token)
    }
}

 

기존 Controller에 logout 작성

더보기
@PostMapping("/logout")
fun logOut(
    @CookieValue("refreshToken") refreshToken: String
): ResponseEntity<String>{
    return ResponseEntity.status(HttpStatus.OK).body(oAuthService.logout(refreshToken))
}

 

기존 Service에 logout 작성

더보기
fun logout(refreshToken: String): String {

    jwtTokenManager.validateRefreshToken(refreshToken)

    val expirationTime = jwtTokenManager.getExpiration(refreshToken)
    tokenBlacklistRepository.addToBlacklist(refreshToken, expirationTime)
    return "로그아웃 성공"
}

 

jwtTokenManger에 로그아웃 기능 작성

( refreshToken이 블랙리스트에 등록되었는지의 로직과 AccessToken이 만료되었을때 RefreshToken을 이용하여 AccessToken을 재발급 하는 로직 추가 )

더보기
fun validateRefreshToken(refreshToken: String){
    validateToken(refreshToken)
        .onSuccess {
            try{
                if(tokenBlacklistRepository.isBlacklisted(refreshToken)){
                    throw IllegalStateException("토큰이 블랙리스트에 등록됨 " )
                }
            } catch(e: Exception){
                throw IllegalStateException("Redis 연결 실패: ${e.message}")
            }
        if(it.payload[TOKEN_TYPE_KEY] == TokenType.ACCESS_TOKEN_TYPE){
            throw IllegalStateException(" refreshToken이 아닌 AccessToken입니다")
        }
    }
        .onFailure {
            throw IllegalStateException("refreshToken 올바르지 않음 ${it.message}")
    }
}

fun reissueAccessToken(refreshToken: String): String{
    val tokenInfo = validateToken(refreshToken).getOrNull()
    val memberId = tokenInfo!!.payload.subject.toLong()
    val memberRole = tokenInfo.payload[MEMBER_ROLE_KEY] as String
    return generateTokenResponse(memberId = memberId, memberRole = memberRole).accessToken
}

 

jwtAuthenticationFilter를 통해 재발급 로직 적용

더보기
.onFailure {
    if (it is ExpiredJwtException) {

        if(refreshToken != null) {
            jwtTokenManager.validateRefreshToken(refreshToken)
            val newAccessToken = jwtTokenManager.reissueAccessToken(refreshToken)
            response.setHeader(HttpHeaders.AUTHORIZATION, "Bearer $newAccessToken")
            val tokenInfo = jwtTokenManager.getInfoToken(newAccessToken)
            val userPrincipal = UserPrincipal(memberId = tokenInfo!!.memberId, memberRole = setOf(tokenInfo.memberRole))
            val authentication = JwtAuthenticationToken(
                userPrincipal = userPrincipal, details = WebAuthenticationDetailsSource().buildDetails(request)
            )
            SecurityContextHolder.getContext().authentication = authentication
        }

        return
    }

토큰 만료시간으로 인해서 validate가 실패한경우 refreshToken을 통해 새로운 AccessToken을 받는다.

 

 

로그아웃 흐름

1. Client에서 로그아웃 버튼을 클릭

2. 쿠키에 저장되어있는 RefreshToken을 Server로 전송

3. Server는 해당 RefreshToken을 Redis를 이용하여 블랙리스트로 등록 

4. Client는 AccessToken/Refresh Token을 삭제하고 메인 페이지로 이동

 

시연 영상: https://www.youtube.com/watch?v=3KqZcLyYgxo