feat: 새로 사용하게 될 subject + claim 기반의 JwtUtils 및 테스트 추가

This commit is contained in:
이상진 2025-09-11 16:49:30 +09:00
parent c9b7c9d4f1
commit da9c7953f4
2 changed files with 162 additions and 0 deletions

View File

@ -0,0 +1,99 @@
package roomescape.auth.infrastructure.jwt
import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException
import java.util.*
import javax.crypto.SecretKey
private val log: KLogger = KotlinLogging.logger {}
@Component
class JwtUtils(
@Value("\${security.jwt.token.secret-key}")
private val secretKeyString: String,
@Value("\${security.jwt.token.ttl-seconds}")
private val tokenTtlSeconds: Long
) {
private val secretKey: SecretKey = Keys.hmacShaKeyFor(secretKeyString.toByteArray())
fun createToken(subject: String, claims: Map<String, Any>): String {
log.debug { "[JwtUtils.createToken] 토큰 생성 시작: subject=$subject, claims=${claims}" }
val date = Date()
val accessTokenExpiredAt = Date(date.time + (tokenTtlSeconds * 1_000))
return Jwts.builder()
.subject(subject)
.claims(claims)
.issuedAt(date)
.expiration(accessTokenExpiredAt)
.signWith(secretKey)
.compact()
.also {
log.debug { "[JwtUtils.createToken] 토큰 생성 완료. token=${it}" }
}
}
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)
}
}
}
fun extractClaim(token: String?, key: String): String {
return runWithHandle {
log.debug { "[JwtUtils.extractClaim] claim 조회 시작: token=$token, claimKey=$key" }
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 <T> 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}" }
throw AuthException(AuthErrorCode.INVALID_TOKEN)
}
}
}

View File

@ -0,0 +1,63 @@
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)
init {
context("종합 테스트") {
test("Subject + Claim을 담아 토큰을 생성한 뒤 읽어온다.") {
val subject = "${tsidFactory.next()}"
val claim = mapOf("name" to "sangdol")
jwtUtils.createToken(subject, claim).also { token ->
val extractedSubject = jwtUtils.extractSubject(token)
val name = jwtUtils.extractClaim(token, "name")
extractedSubject shouldBe subject
name shouldBe "sangdol"
}
}
}
context("실패 테스트") {
val subject = "${tsidFactory.next()}"
val claim = mapOf("name" to "sangdol")
val commonToken = jwtUtils.createToken(subject, claim)
context("subject를 가져올 때 null 토큰을 입력하면 실패한다.") {
shouldThrow<AuthException> {
jwtUtils.extractSubject(null)
}.also {
it.errorCode shouldBe AuthErrorCode.TOKEN_NOT_FOUND
}
}
context("claim을 가져올 때 null 토큰을 입력하면 실패한다.") {
shouldThrow<AuthException> {
jwtUtils.extractClaim(token = null, key = "")
}.also {
it.errorCode shouldBe AuthErrorCode.TOKEN_NOT_FOUND
}
}
context("토큰은 유효하나 claim이 없으면 실패한다.") {
shouldThrow<AuthException> {
jwtUtils.extractClaim(token = commonToken, key = "abcde")
}.also {
it.errorCode shouldBe AuthErrorCode.MEMBER_NOT_FOUND
}
}
}
}
}