refactor: JwtUtils에서의 공통 부분 메서드 분리 & 만료 조건 추가 및 테스트

This commit is contained in:
이상진 2025-09-12 20:52:26 +09:00
parent ea45673ef4
commit e4f6ffe53d
2 changed files with 60 additions and 48 deletions

View File

@ -2,6 +2,7 @@ package roomescape.auth.infrastructure.jwt
import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.Keys
@ -43,56 +44,37 @@ class JwtUtils(
} }
fun extractSubject(token: String?): String { fun extractSubject(token: String?): String {
return runWithHandle { if (token.isNullOrBlank()) {
log.debug { "[JwtUtils.extractSubject] subject 조회 시작: token=$token" } throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
}
val claims = extractAllClaims(token)
Jwts.parser() return claims.subject ?: throw AuthException(AuthErrorCode.INVALID_TOKEN)
.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)
}
}
} }
fun extractClaim(token: String?, key: String): String { fun extractClaim(token: String?, key: String): String {
return runWithHandle { if (token.isNullOrBlank()) {
log.debug { "[JwtUtils.extractClaim] claim 조회 시작: token=$token, claimKey=$key" } 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) .verifyWith(secretKey)
.build() .build()
.parseSignedClaims(token) .parseSignedClaims(token)
.payload .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 <T> runWithHandle(block: () -> T): T {
try {
return block()
} catch (e: AuthException) {
throw e
} catch (_: IllegalArgumentException) {
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
} catch (_: ExpiredJwtException) { } catch (_: ExpiredJwtException) {
throw AuthException(AuthErrorCode.EXPIRED_TOKEN) throw AuthException(AuthErrorCode.EXPIRED_TOKEN)
} catch (e: Exception) { } catch (ex: Exception) {
log.warn { "[JwtUtils] 예외 발생: message=${e.message}" } log.warn { "[JwtUtils] 유효하지 않은 토큰 요청: ${ex.message}" }
throw AuthException(AuthErrorCode.INVALID_TOKEN) throw AuthException(AuthErrorCode.INVALID_TOKEN)
} }
} }

View File

@ -3,16 +3,17 @@ package roomescape.auth
import io.kotest.assertions.throwables.shouldThrow import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.assertThrows
import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException import roomescape.auth.exception.AuthException
import roomescape.auth.infrastructure.jwt.JwtUtils import roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.common.config.next import roomescape.common.config.next
import roomescape.util.tsidFactory import roomescape.util.tsidFactory
class JwtUtilsTest( class JwtUtilsTest : FunSpec() {
) : FunSpec() { private val jwtUtils: JwtUtils = JwtUtils(
private val jwtUtils: JwtUtils = JwtUtils(secretKeyString = "caSf+JhhY9J9VcZxDQ7SNNOEIAJSZ9onsFstGNv9bjPHmHoTTcX+5wway5+//SPi", tokenTtlSeconds = 5) secretKeyString = "caSf+JhhY9J9VcZxDQ7SNNOEIAJSZ9onsFstGNv9bjPHmHoTTcX+5wway5+//SPi",
tokenTtlSeconds = 5L
)
init { init {
context("종합 테스트") { context("종합 테스트") {
@ -35,7 +36,7 @@ class JwtUtilsTest(
val claim = mapOf("name" to "sangdol") val claim = mapOf("name" to "sangdol")
val commonToken = jwtUtils.createToken(subject, claim) val commonToken = jwtUtils.createToken(subject, claim)
context("subject를 가져올 때 null 토큰을 입력하면 실패한다.") { test("subject를 가져올 때 null 토큰을 입력하면 실패한다.") {
shouldThrow<AuthException> { shouldThrow<AuthException> {
jwtUtils.extractSubject(null) jwtUtils.extractSubject(null)
}.also { }.also {
@ -43,7 +44,7 @@ class JwtUtilsTest(
} }
} }
context("claim을 가져올 때 null 토큰을 입력하면 실패한다.") { test("claim을 가져올 때 null 토큰을 입력하면 실패한다.") {
shouldThrow<AuthException> { shouldThrow<AuthException> {
jwtUtils.extractClaim(token = null, key = "") jwtUtils.extractClaim(token = null, key = "")
}.also { }.also {
@ -51,11 +52,40 @@ class JwtUtilsTest(
} }
} }
context("토큰은 유효하나 claim이 없으면 실패한다.") { test("claim에 입력된 key의 정보가 없으면 실패한다.") {
shouldThrow<AuthException> { shouldThrow<AuthException> {
jwtUtils.extractClaim(token = commonToken, key = "abcde") jwtUtils.extractClaim(token = commonToken, key = "abcde")
}.also { }.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<AuthException> {
jwtUtil.extractSubject(token)
}.also {
it.errorCode shouldBe AuthErrorCode.EXPIRED_TOKEN
}
shouldThrow<AuthException> {
jwtUtil.extractClaim(token, key = "name")
}.also {
it.errorCode shouldBe AuthErrorCode.EXPIRED_TOKEN
}
}
test("토큰 형식이 잘못되면 실패한다.") {
shouldThrow<AuthException> {
jwtUtils.extractSubject("abcde")
}.also {
it.errorCode shouldBe AuthErrorCode.INVALID_TOKEN
} }
} }
} }