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 import org.slf4j.MDC import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import roomescape.auth.business.CLAIM_TYPE_KEY import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthException import roomescape.common.dto.MDC_PRINCIPAL_ID_KEY import roomescape.common.dto.PrincipalType 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 { 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 extractIdAndType(token: String?): Pair { val id: Long = extractSubject(token) .also { MDC.put(MDC_PRINCIPAL_ID_KEY, it) } .toLong() val type: PrincipalType = extractClaim(token, CLAIM_TYPE_KEY) ?.let { PrincipalType.valueOf(it) } ?: run { log.info { "[JwtUtils.extractIdAndType] 회원 타입 조회 실패. id=$id" } throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND) } return id to type } fun extractSubject(token: String?): String { if (token.isNullOrBlank()) { throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) } val claims = extractAllClaims(token) return claims.subject ?: run { log.info { "[JwtUtils.extractSubject] subject를 찾을 수 없음.: token = ${token}" } throw AuthException(AuthErrorCode.INVALID_TOKEN) } } fun extractClaim(token: String?, key: String): String? { if (token.isNullOrBlank()) { throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) } val claims = extractAllClaims(token) return claims.get(key, String::class.java) } private fun extractAllClaims(token: String): Claims { try { return Jwts.parser() .verifyWith(secretKey) .build() .parseSignedClaims(token) .payload } catch (_: ExpiredJwtException) { throw AuthException(AuthErrorCode.EXPIRED_TOKEN) } catch (ex: Exception) { log.warn { "[JwtUtils] 유효하지 않은 토큰 요청: ${ex.message}" } throw AuthException(AuthErrorCode.INVALID_TOKEN) } } }