generated from pricelees/issue-pr-template
[#34] 회원 / 인증 도메인 재정의 #43
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
src/test/kotlin/roomescape/auth/JwtUtilsTest.kt
Normal file
63
src/test/kotlin/roomescape/auth/JwtUtilsTest.kt
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user