diff --git a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtUtils.kt b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtUtils.kt index 88fd9b18..1ba9a96e 100644 --- a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtUtils.kt +++ b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtUtils.kt @@ -2,6 +2,7 @@ package roomescape.auth.infrastructure.jwt import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging +import io.jsonwebtoken.Claims import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys @@ -43,56 +44,37 @@ class JwtUtils( } fun extractSubject(token: String?): String { - return runWithHandle { - log.debug { "[JwtUtils.extractSubject] subject 조회 시작: token=$token" } - - Jwts.parser() - .verifyWith(secretKey) - .build() - .parseSignedClaims(token) - .payload - .subject - ?.also { - log.debug { "[JwtUtils.extractSubject] subject 조회 완료: subject=${it}" } - } - ?: run { - log.debug { "[JwtUtils.extractSubject] subject 조회 실패: token=${token}" } - throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND) - } + if (token.isNullOrBlank()) { + throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) } + val claims = extractAllClaims(token) + + return claims.subject ?: throw AuthException(AuthErrorCode.INVALID_TOKEN) } fun extractClaim(token: String?, key: String): String { - return runWithHandle { - log.debug { "[JwtUtils.extractClaim] claim 조회 시작: token=$token, claimKey=$key" } + if (token.isNullOrBlank()) { + throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) + } + val claims = extractAllClaims(token) - Jwts.parser() + return claims.get(key, String::class.java) ?: run { + log.warn { "[JwtUtils] Claim 조회 실패: key=$key" } + throw AuthException(AuthErrorCode.INVALID_TOKEN) + } + } + + private fun extractAllClaims(token: String): Claims { + try { + return Jwts.parser() .verifyWith(secretKey) .build() .parseSignedClaims(token) .payload - .get(key, String::class.java) - ?.also { - log.debug { "[JwtHandler.extractClaim] claim 조회 완료: claim=${it}" } - } - ?: run { - log.info { "[JwtUtils.extractClaim] claim=${key} 조회 실패: token=$token" } - throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND) - } - } - } - - private fun runWithHandle(block: () -> T): T { - try { - return block() - } catch (e: AuthException) { - throw e - } catch (_: IllegalArgumentException) { - throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) } catch (_: ExpiredJwtException) { throw AuthException(AuthErrorCode.EXPIRED_TOKEN) - } catch (e: Exception) { - log.warn { "[JwtUtils] 예외 발생: message=${e.message}" } + } catch (ex: Exception) { + log.warn { "[JwtUtils] 유효하지 않은 토큰 요청: ${ex.message}" } throw AuthException(AuthErrorCode.INVALID_TOKEN) } } diff --git a/src/test/kotlin/roomescape/auth/JwtUtilsTest.kt b/src/test/kotlin/roomescape/auth/JwtUtilsTest.kt index 627ed8dd..f92c5c56 100644 --- a/src/test/kotlin/roomescape/auth/JwtUtilsTest.kt +++ b/src/test/kotlin/roomescape/auth/JwtUtilsTest.kt @@ -3,16 +3,17 @@ package roomescape.auth import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import org.junit.jupiter.api.assertThrows import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthException import roomescape.auth.infrastructure.jwt.JwtUtils import roomescape.common.config.next import roomescape.util.tsidFactory -class JwtUtilsTest( -) : FunSpec() { - private val jwtUtils: JwtUtils = JwtUtils(secretKeyString = "caSf+JhhY9J9VcZxDQ7SNNOEIAJSZ9onsFstGNv9bjPHmHoTTcX+5wway5+//SPi", tokenTtlSeconds = 5) +class JwtUtilsTest : FunSpec() { + private val jwtUtils: JwtUtils = JwtUtils( + secretKeyString = "caSf+JhhY9J9VcZxDQ7SNNOEIAJSZ9onsFstGNv9bjPHmHoTTcX+5wway5+//SPi", + tokenTtlSeconds = 5L + ) init { context("종합 테스트") { @@ -35,7 +36,7 @@ class JwtUtilsTest( val claim = mapOf("name" to "sangdol") val commonToken = jwtUtils.createToken(subject, claim) - context("subject를 가져올 때 null 토큰을 입력하면 실패한다.") { + test("subject를 가져올 때 null 토큰을 입력하면 실패한다.") { shouldThrow { jwtUtils.extractSubject(null) }.also { @@ -43,7 +44,7 @@ class JwtUtilsTest( } } - context("claim을 가져올 때 null 토큰을 입력하면 실패한다.") { + test("claim을 가져올 때 null 토큰을 입력하면 실패한다.") { shouldThrow { jwtUtils.extractClaim(token = null, key = "") }.also { @@ -51,13 +52,42 @@ class JwtUtilsTest( } } - context("토큰은 유효하나 claim이 없으면 실패한다.") { + test("claim에 입력된 key의 정보가 없으면 실패한다.") { shouldThrow { jwtUtils.extractClaim(token = commonToken, key = "abcde") }.also { - it.errorCode shouldBe AuthErrorCode.MEMBER_NOT_FOUND + it.errorCode shouldBe AuthErrorCode.INVALID_TOKEN + } + } + + test("토큰이 만료되면 실패한다.") { + val jwtUtil = JwtUtils( + secretKeyString = "caSf+JhhY9J9VcZxDQ7SNNOEIAJSZ9onsFstGNv9bjPHmHoTTcX+5wway5+//SPi", + tokenTtlSeconds = 0L + ) + + val token = jwtUtil.createToken("hello", mapOf("name" to "sangdol")) + + shouldThrow { + jwtUtil.extractSubject(token) + }.also { + it.errorCode shouldBe AuthErrorCode.EXPIRED_TOKEN + } + + shouldThrow { + jwtUtil.extractClaim(token, key = "name") + }.also { + it.errorCode shouldBe AuthErrorCode.EXPIRED_TOKEN + } + } + + test("토큰 형식이 잘못되면 실패한다.") { + shouldThrow { + jwtUtils.extractSubject("abcde") + }.also { + it.errorCode shouldBe AuthErrorCode.INVALID_TOKEN } } } } -} \ No newline at end of file +}