refactor: JwtUtils에 Inerceptor / Resolver 공통 로직 생성 및 null claim 조회 시 로그 추가

This commit is contained in:
이상진 2025-09-13 11:46:22 +09:00
parent 26910f1d14
commit 2fc1cabe0e
5 changed files with 53 additions and 43 deletions

View File

@ -6,10 +6,14 @@ 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
import org.slf4j.MDC
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import roomescape.auth.business.CLAIM_TYPE_KEY
import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthErrorCode
import roomescape.auth.exception.AuthException import roomescape.auth.exception.AuthException
import roomescape.common.dto.MDC_PRINCIPAL_ID_KEY
import roomescape.common.dto.PrincipalType
import java.util.* import java.util.*
import javax.crypto.SecretKey import javax.crypto.SecretKey
@ -43,25 +47,40 @@ class JwtUtils(
} }
} }
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 { fun extractSubject(token: String?): String {
if (token.isNullOrBlank()) { if (token.isNullOrBlank()) {
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
} }
val claims = extractAllClaims(token) val claims = extractAllClaims(token)
return claims.subject ?: throw AuthException(AuthErrorCode.INVALID_TOKEN) return claims.subject ?: run {
log.info { "[JwtUtils.extractSubject] subject를 찾을 수 없음.: token = ${token}" }
throw AuthException(AuthErrorCode.INVALID_TOKEN)
}
} }
fun extractClaim(token: String?, key: String): String { fun extractClaim(token: String?, key: String): String? {
if (token.isNullOrBlank()) { if (token.isNullOrBlank()) {
throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND)
} }
val claims = extractAllClaims(token) val claims = extractAllClaims(token)
return claims.get(key, String::class.java) ?: run { return claims.get(key, String::class.java)
log.warn { "[JwtUtils] Claim 조회 실패: key=$key" }
throw AuthException(AuthErrorCode.INVALID_TOKEN)
}
} }
private fun extractAllClaims(token: String): Claims { private fun extractAllClaims(token: String): Claims {

View File

@ -4,7 +4,6 @@ import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.slf4j.MDC
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.HandlerInterceptor
@ -14,8 +13,8 @@ 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.auth.web.support.AdminOnly import roomescape.auth.web.support.AdminOnly
import roomescape.auth.web.support.MDC_MEMBER_ID_KEY
import roomescape.auth.web.support.accessToken import roomescape.auth.web.support.accessToken
import roomescape.common.dto.PrincipalType
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -35,20 +34,28 @@ class AdminInterceptor(
val annotation: AdminOnly = handler.getMethodAnnotation(AdminOnly::class.java) ?: return true val annotation: AdminOnly = handler.getMethodAnnotation(AdminOnly::class.java) ?: return true
val token: String? = request.accessToken() val token: String? = request.accessToken()
val adminId = jwtUtils.extractSubject(token).also { MDC.put(MDC_MEMBER_ID_KEY, it) } val (id, type) = jwtUtils.extractIdAndType(token)
jwtUtils.extractClaim( val permission: AdminPermissionLevel = jwtUtils.extractClaim(token, key = CLAIM_PERMISSION_KEY)
token = token, key = CLAIM_PERMISSION_KEY ?.let {
).also { AdminPermissionLevel.valueOf(it)
val permission = AdminPermissionLevel.valueOf(it)
if (!permission.hasPrivilege(annotation.privilege)) {
log.warn { "[AuthInterceptor] 관리자 권한 부족: required=${annotation.privilege} / current=${permission}" }
throw AuthException(AuthErrorCode.ACCESS_DENIED)
} }
log.info { "[AuthInterceptor] 인증 완료. adminId=$adminId, permission=${permission}" } ?: run {
if (type != PrincipalType.ADMIN) {
log.warn { "[AdminInterceptor] 회원의 관리자 API 접근: id=${id}" }
throw AuthException(AuthErrorCode.ACCESS_DENIED)
}
log.warn { "[AdminInterceptor] 토큰에서 이용자 권한이 조회되지 않음: id=${id}" }
throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND)
}
if (!permission.hasPrivilege(annotation.privilege)) {
log.warn { "[AdminInterceptor] 관리자 권한 부족: required=${annotation.privilege} / current=${permission}" }
throw AuthException(AuthErrorCode.ACCESS_DENIED)
} }
log.info { "[AdminInterceptor] 인증 완료. adminId=$id, permission=${permission}" }
return true return true
} }
} }

View File

@ -4,17 +4,13 @@ import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.slf4j.MDC
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.HandlerInterceptor
import roomescape.auth.business.AuthServiceV2 import roomescape.auth.business.AuthServiceV2
import roomescape.auth.business.CLAIM_TYPE_KEY
import roomescape.auth.infrastructure.jwt.JwtUtils import roomescape.auth.infrastructure.jwt.JwtUtils
import roomescape.auth.web.support.Authenticated import roomescape.auth.web.support.Authenticated
import roomescape.auth.web.support.MDC_MEMBER_ID_KEY
import roomescape.auth.web.support.accessToken import roomescape.auth.web.support.accessToken
import roomescape.common.dto.PrincipalType
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -34,12 +30,10 @@ class AuthenticatedInterceptor(
} }
val token: String? = request.accessToken() val token: String? = request.accessToken()
val (id, type) = jwtUtils.extractIdAndType(token)
val id = jwtUtils.extractSubject(token).also { MDC.put(MDC_MEMBER_ID_KEY, it) }
val type = jwtUtils.extractClaim(token, CLAIM_TYPE_KEY)
try { try {
authService.findContextById(id.toLong(), PrincipalType.valueOf(type)) authService.findContextById(id, type)
log.info { "[AuthenticatedInterceptor] 인증 완료. id=$id, type=${type}" } log.info { "[AuthenticatedInterceptor] 인증 완료. id=$id, type=${type}" }
return true return true

View File

@ -4,15 +4,12 @@ import io.github.oshai.kotlinlogging.KLogger
import io.github.oshai.kotlinlogging.KotlinLogging import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse import jakarta.servlet.http.HttpServletResponse
import org.slf4j.MDC
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import org.springframework.web.method.HandlerMethod import org.springframework.web.method.HandlerMethod
import org.springframework.web.servlet.HandlerInterceptor import org.springframework.web.servlet.HandlerInterceptor
import roomescape.auth.business.CLAIM_TYPE_KEY
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.auth.web.support.MDC_MEMBER_ID_KEY
import roomescape.auth.web.support.UserOnly import roomescape.auth.web.support.UserOnly
import roomescape.auth.web.support.accessToken import roomescape.auth.web.support.accessToken
import roomescape.common.dto.PrincipalType import roomescape.common.dto.PrincipalType
@ -34,17 +31,14 @@ class UserInterceptor(
} }
val token: String? = request.accessToken() val token: String? = request.accessToken()
val userId = jwtUtils.extractSubject(token).also { id -> MDC.put(MDC_MEMBER_ID_KEY, id) } val (id, type) = jwtUtils.extractIdAndType(token)
jwtUtils.extractClaim(token, CLAIM_TYPE_KEY).also { if (type != PrincipalType.USER) {
if (it != PrincipalType.USER.name) { log.warn { "[UserInterceptor] 관리자의 회원 API 접근: id=${id}" }
log.warn { "[UserInterceptor] 관리자의 회원 API 접근: id=${userId}" } throw AuthException(AuthErrorCode.ACCESS_DENIED)
throw AuthException(AuthErrorCode.ACCESS_DENIED)
}
} }
log.info { "[AuthInterceptor] 인증 완료. userId=$userId" } log.info { "[UserInterceptor] 인증 완료. userId=$id" }
return true return true
} }
} }

View File

@ -10,13 +10,11 @@ import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer import org.springframework.web.method.support.ModelAndViewContainer
import roomescape.auth.business.AuthServiceV2 import roomescape.auth.business.AuthServiceV2
import roomescape.auth.business.CLAIM_TYPE_KEY
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.auth.web.support.CurrentUser import roomescape.auth.web.support.CurrentUser
import roomescape.auth.web.support.accessToken import roomescape.auth.web.support.accessToken
import roomescape.common.dto.PrincipalType
private val log: KLogger = KotlinLogging.logger {} private val log: KLogger = KotlinLogging.logger {}
@ -40,14 +38,12 @@ class CurrentUserContextResolver(
val token: String? = request.accessToken() val token: String? = request.accessToken()
try { try {
val id: String = jwtUtils.extractSubject(token) val (id, type) = jwtUtils.extractIdAndType(token)
val type: PrincipalType = PrincipalType.valueOf(jwtUtils.extractClaim(token, CLAIM_TYPE_KEY))
return authService.findContextById(id.toLong(), type) return authService.findContextById(id, type)
} catch (e: Exception) { } catch (e: Exception) {
log.info { "[MemberIdResolver] 회원 조회 실패. message=${e.message}" } log.info { "[MemberIdResolver] 회원 조회 실패. message=${e.message}" }
throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND) throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND)
} }
} }
} }