[#34] 회원 / 인증 도메인 재정의 #43

Merged
pricelees merged 73 commits from refactor/#34 into main 2025-09-13 10:13:45 +00:00
5 changed files with 53 additions and 43 deletions
Showing only changes of commit 2fc1cabe0e - Show all commits

View File

@ -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<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 ?: 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 {

View File

@ -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}" }
val permission: AdminPermissionLevel = jwtUtils.extractClaim(token, key = CLAIM_PERMISSION_KEY)
?.let {
AdminPermissionLevel.valueOf(it)
}
?: run {
if (type != PrincipalType.ADMIN) {
log.warn { "[AdminInterceptor] 회원의 관리자 API 접근: id=${id}" }
throw AuthException(AuthErrorCode.ACCESS_DENIED)
}
log.info { "[AuthInterceptor] 인증 완료. adminId=$adminId, permission=${permission}" }
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
}
}

View File

@ -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

View File

@ -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}" }
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
}
}

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.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)
}
}
}