From 2fc1cabe0e254fbbe3b27bd0ffada0f417571af7 Mon Sep 17 00:00:00 2001 From: pricelees Date: Sat, 13 Sep 2025 11:46:22 +0900 Subject: [PATCH] =?UTF-8?q?refactor:=20JwtUtils=EC=97=90=20Inerceptor=20/?= =?UTF-8?q?=20Resolver=20=EA=B3=B5=ED=86=B5=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20null=20claim=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/infrastructure/jwt/JwtUtils.kt | 31 +++++++++++++++---- .../support/interceptors/AdminInterceptor.kt | 31 ++++++++++++------- .../interceptors/AuthenticatedInterceptor.kt | 10 ++---- .../support/interceptors/UserInterceptor.kt | 16 +++------- .../resolver/CurrentUserContextResolver.kt | 8 ++--- 5 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtUtils.kt b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtUtils.kt index 1ba9a96e..8e74a755 100644 --- a/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtUtils.kt +++ b/src/main/kotlin/roomescape/auth/infrastructure/jwt/JwtUtils.kt @@ -6,10 +6,14 @@ 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 @@ -43,25 +47,40 @@ class JwtUtils( } } + 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 ?: 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()) { throw AuthException(AuthErrorCode.TOKEN_NOT_FOUND) } val claims = extractAllClaims(token) - return claims.get(key, String::class.java) ?: run { - log.warn { "[JwtUtils] Claim 조회 실패: key=$key" } - throw AuthException(AuthErrorCode.INVALID_TOKEN) - } + return claims.get(key, String::class.java) } private fun extractAllClaims(token: String): Claims { diff --git a/src/main/kotlin/roomescape/auth/web/support/interceptors/AdminInterceptor.kt b/src/main/kotlin/roomescape/auth/web/support/interceptors/AdminInterceptor.kt index 6361fdca..b9f0cc63 100644 --- a/src/main/kotlin/roomescape/auth/web/support/interceptors/AdminInterceptor.kt +++ b/src/main/kotlin/roomescape/auth/web/support/interceptors/AdminInterceptor.kt @@ -4,7 +4,6 @@ import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import org.slf4j.MDC import org.springframework.stereotype.Component import org.springframework.web.method.HandlerMethod import org.springframework.web.servlet.HandlerInterceptor @@ -14,8 +13,8 @@ import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthException import roomescape.auth.infrastructure.jwt.JwtUtils import roomescape.auth.web.support.AdminOnly -import roomescape.auth.web.support.MDC_MEMBER_ID_KEY import roomescape.auth.web.support.accessToken +import roomescape.common.dto.PrincipalType private val log: KLogger = KotlinLogging.logger {} @@ -35,20 +34,28 @@ class AdminInterceptor( val annotation: AdminOnly = handler.getMethodAnnotation(AdminOnly::class.java) ?: return true 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( - token = token, key = CLAIM_PERMISSION_KEY - ).also { - val permission = AdminPermissionLevel.valueOf(it) - - if (!permission.hasPrivilege(annotation.privilege)) { - log.warn { "[AuthInterceptor] 관리자 권한 부족: required=${annotation.privilege} / current=${permission}" } - throw AuthException(AuthErrorCode.ACCESS_DENIED) + val permission: AdminPermissionLevel = jwtUtils.extractClaim(token, key = CLAIM_PERMISSION_KEY) + ?.let { + AdminPermissionLevel.valueOf(it) } - 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 } } diff --git a/src/main/kotlin/roomescape/auth/web/support/interceptors/AuthenticatedInterceptor.kt b/src/main/kotlin/roomescape/auth/web/support/interceptors/AuthenticatedInterceptor.kt index ff248ff1..90ee6b45 100644 --- a/src/main/kotlin/roomescape/auth/web/support/interceptors/AuthenticatedInterceptor.kt +++ b/src/main/kotlin/roomescape/auth/web/support/interceptors/AuthenticatedInterceptor.kt @@ -4,17 +4,13 @@ import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import org.slf4j.MDC import org.springframework.stereotype.Component import org.springframework.web.method.HandlerMethod import org.springframework.web.servlet.HandlerInterceptor import roomescape.auth.business.AuthServiceV2 -import roomescape.auth.business.CLAIM_TYPE_KEY import roomescape.auth.infrastructure.jwt.JwtUtils import roomescape.auth.web.support.Authenticated -import roomescape.auth.web.support.MDC_MEMBER_ID_KEY import roomescape.auth.web.support.accessToken -import roomescape.common.dto.PrincipalType private val log: KLogger = KotlinLogging.logger {} @@ -34,12 +30,10 @@ class AuthenticatedInterceptor( } val token: String? = request.accessToken() - - val id = jwtUtils.extractSubject(token).also { MDC.put(MDC_MEMBER_ID_KEY, it) } - val type = jwtUtils.extractClaim(token, CLAIM_TYPE_KEY) + val (id, type) = jwtUtils.extractIdAndType(token) try { - authService.findContextById(id.toLong(), PrincipalType.valueOf(type)) + authService.findContextById(id, type) log.info { "[AuthenticatedInterceptor] 인증 완료. id=$id, type=${type}" } return true diff --git a/src/main/kotlin/roomescape/auth/web/support/interceptors/UserInterceptor.kt b/src/main/kotlin/roomescape/auth/web/support/interceptors/UserInterceptor.kt index c350f2ec..ac42cd0f 100644 --- a/src/main/kotlin/roomescape/auth/web/support/interceptors/UserInterceptor.kt +++ b/src/main/kotlin/roomescape/auth/web/support/interceptors/UserInterceptor.kt @@ -4,15 +4,12 @@ import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import org.slf4j.MDC import org.springframework.stereotype.Component import org.springframework.web.method.HandlerMethod import org.springframework.web.servlet.HandlerInterceptor -import roomescape.auth.business.CLAIM_TYPE_KEY import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthException 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.accessToken import roomescape.common.dto.PrincipalType @@ -34,17 +31,14 @@ class UserInterceptor( } 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 (it != PrincipalType.USER.name) { - log.warn { "[UserInterceptor] 관리자의 회원 API 접근: id=${userId}" } - throw AuthException(AuthErrorCode.ACCESS_DENIED) - } + if (type != PrincipalType.USER) { + log.warn { "[UserInterceptor] 관리자의 회원 API 접근: id=${id}" } + throw AuthException(AuthErrorCode.ACCESS_DENIED) } - log.info { "[AuthInterceptor] 인증 완료. userId=$userId" } - + log.info { "[UserInterceptor] 인증 완료. userId=$id" } return true } } diff --git a/src/main/kotlin/roomescape/auth/web/support/resolver/CurrentUserContextResolver.kt b/src/main/kotlin/roomescape/auth/web/support/resolver/CurrentUserContextResolver.kt index a5024aa1..a4ca2652 100644 --- a/src/main/kotlin/roomescape/auth/web/support/resolver/CurrentUserContextResolver.kt +++ b/src/main/kotlin/roomescape/auth/web/support/resolver/CurrentUserContextResolver.kt @@ -10,13 +10,11 @@ import org.springframework.web.context.request.NativeWebRequest import org.springframework.web.method.support.HandlerMethodArgumentResolver import org.springframework.web.method.support.ModelAndViewContainer import roomescape.auth.business.AuthServiceV2 -import roomescape.auth.business.CLAIM_TYPE_KEY import roomescape.auth.exception.AuthErrorCode import roomescape.auth.exception.AuthException import roomescape.auth.infrastructure.jwt.JwtUtils import roomescape.auth.web.support.CurrentUser import roomescape.auth.web.support.accessToken -import roomescape.common.dto.PrincipalType private val log: KLogger = KotlinLogging.logger {} @@ -40,14 +38,12 @@ class CurrentUserContextResolver( val token: String? = request.accessToken() try { - val id: String = jwtUtils.extractSubject(token) - val type: PrincipalType = PrincipalType.valueOf(jwtUtils.extractClaim(token, CLAIM_TYPE_KEY)) + val (id, type) = jwtUtils.extractIdAndType(token) - return authService.findContextById(id.toLong(), type) + return authService.findContextById(id, type) } catch (e: Exception) { log.info { "[MemberIdResolver] 회원 조회 실패. message=${e.message}" } throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND) } } } -