generated from pricelees/issue-pr-template
<!-- 제목 양식 --> <!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) --> ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #34 ## ✨ 작업 내용 <!-- 어떤 작업을 했는지 알려주세요! --> - 회원 테이블과 관리자 테이블 분리 및 관리자 계정의 예약 기능 제거 - API 인증을 모두(Public) / 회원 전용(UserOnly) / 관리자 전용(AdminOnly) / 회원 + 관리자(Authenticated) 로 세분화해서 구분 - 관리자의 경우 API 접근 권한 세분화 등 인증 로직 개선 - 전체 리팩터링이 완료되어 레거시 코드 제거 ## 🧪 테스트 <!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! --> <img width="750" alt="스크린샷 2025-09-13 19.11.44.png" src="attachments/11e1a79c-9723-4843-839d-be6158d94130"> - 추가 & 변경된 모든 API에 대한 통합 테스트 진행 ## 📚 참고 자료 및 기타 <!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! --> Reviewed-on: #43 Co-authored-by: pricelees <priceelees@gmail.com> Co-committed-by: pricelees <priceelees@gmail.com>
101 lines
3.4 KiB
Kotlin
101 lines
3.4 KiB
Kotlin
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, 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 extractIdAndType(token: String?): Pair<Long, PrincipalType> {
|
|
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)
|
|
}
|
|
}
|
|
}
|