Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REFACTOR]: TokenValidator와 TokenGenerator 추가로 TokenService 가독성 증대 및 패키지 경로 수정 #109

Merged
merged 4 commits into from
Aug 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class JwtProvider(
) {
fun validateAccessTokenFromRequest(servletRequest: ServletRequest, token: String?): Boolean {
try {
tokenValidator.validateWithSecretKey(
tokenValidator.validate(
Keys.hmacShaKeyFor(jwtProperty.accessToken.secret.toByteArray()),
token!!,
).subject.toLong()
Expand All @@ -34,7 +34,7 @@ class JwtProvider(
var user: User? = null
try {
val userId =
tokenValidator.validateWithSecretKey(
tokenValidator.validate(
Keys.hmacShaKeyFor(jwtProperty.accessToken.secret.toByteArray()),
token!!,
).subject.toLong()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ enum class CoreErrorType(
* 토큰 관련 에러
*/
UNAUTHORIZED_TOKEN(CoreErrorKind.SERVER_ERROR, CoreErrorCode.TOKEN0001, "토큰이 유효하지 않습니다. 로그인을 다시 해주세요.", CoreErrorLevel.INFO),
NOT_FOUND_TOKEN(CoreErrorKind.SERVER_ERROR, CoreErrorCode.TOKEN0002, "이미 로그아웃 된 유저입니다. 로그인을 다시 해주세요.", CoreErrorLevel.INFO),
TOKEN_NOT_FOUND(CoreErrorKind.SERVER_ERROR, CoreErrorCode.TOKEN0002, "이미 로그아웃 된 유저입니다. 로그인을 다시 해주세요.", CoreErrorLevel.INFO),

/*
* 공지사항 관련 에러
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.myongjiway.core.domain.token

import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.Jwts.SIG.HS512
import io.jsonwebtoken.security.Keys
Expand All @@ -9,6 +10,8 @@ import java.util.Date
@Component
class TokenGenerator(
private val jwtProperty: JwtProperty,
private val tokenValidator: TokenValidator,
private val tokenAppender: TokenAppender,
) {
fun generateAccessTokenByUserId(userId: String): Token {
val (expiration, secret) = getExpirationAndSecret(TokenType.ACCESS)
Expand Down Expand Up @@ -37,6 +40,18 @@ class TokenGenerator(
)
}

fun refresh(refreshToken: Token): Token {
val secret = getExpirationAndSecret(TokenType.REFRESH).second
try {
tokenValidator.validate(Keys.hmacShaKeyFor(secret.toByteArray()), refreshToken.token)
} catch (e: ExpiredJwtException) {
val newRefreshToken = generateRefreshTokenByUserId(refreshToken.userId)
tokenAppender.upsert(refreshToken.userId.toLong(), newRefreshToken.token, newRefreshToken.expiration)
return newRefreshToken
}
return refreshToken
}

private fun getExpirationAndSecret(tokenType: TokenType) = when (tokenType) {
TokenType.ACCESS -> jwtProperty.accessToken.expiration to jwtProperty.accessToken.secret
TokenType.REFRESH -> jwtProperty.refreshToken.expiration to jwtProperty.refreshToken.secret
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.myongjiway.core.domain.token

import com.myongjiway.core.domain.error.CoreErrorType
import com.myongjiway.core.domain.error.CoreException
import org.springframework.stereotype.Component

@Component
class TokenReader(
private val tokenRepository: TokenRepository,
) {
fun findByToken(token: String): Token? = tokenRepository.find(token)
fun find(token: String): Token = tokenRepository.find(token) ?: throw CoreException(CoreErrorType.TOKEN_NOT_FOUND)
}
Original file line number Diff line number Diff line change
@@ -1,41 +1,33 @@
package com.myongjiway.core.domain.token

import com.myongjiway.core.domain.error.CoreErrorType
import com.myongjiway.core.domain.user.UserReader
import org.springframework.stereotype.Service

@Service
class TokenService(
private val tokenAppender: TokenAppender,
private val tokenGenerator: TokenGenerator,
private val tokenReader: TokenReader,
private val userReader: UserReader,
private val tokenProcessor: TokenProcessor,
) {
/**
* RefreshToken을 이용하여 AccessToken을 재발급한다. RefreshToken 또한 만료되었다면 갱신한다.
* @param refreshData AccessToken을 재발급하기 위한 RefreshToken
* @return TokenResult 재발급된 AccessToken과 RefreshToken
*/
fun refresh(refreshData: RefreshData): TokenResult {
var refreshToken = tokenReader.findByToken(refreshData.refreshToken)
?: throw com.myongjiway.core.domain.error.CoreException(CoreErrorType.UNAUTHORIZED_TOKEN)
var refreshToken = tokenReader.find(refreshData.refreshToken)

val user = userReader.find(refreshToken.userId.toLong())
?: throw com.myongjiway.core.domain.error.CoreException(CoreErrorType.USER_NOT_FOUND)

if (isExpired(refreshToken)) {
refreshToken = tokenGenerator.generateRefreshTokenByUserId(user.id.toString())
tokenAppender.upsert(user.id, refreshToken.token, refreshToken.expiration)
}
refreshToken = tokenGenerator.refresh(refreshToken)

val newAccessToken = tokenGenerator.generateAccessTokenByUserId(user.id.toString())
return TokenResult(newAccessToken.token, refreshToken.token)
}

fun delete(refreshToken: String) {
val findRefreshToken = (
tokenReader.findByToken(refreshToken)
?: throw com.myongjiway.core.domain.error.CoreException(CoreErrorType.NOT_FOUND_TOKEN)
)

tokenProcessor.deleteToken(findRefreshToken.token)
val foundRefreshToken = tokenReader.find(refreshToken)
tokenProcessor.deleteToken(foundRefreshToken.token)
}

private fun isExpired(refreshToken: Token?): Boolean = refreshToken?.expiration!! <= System.currentTimeMillis()
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import javax.crypto.SecretKey

@Component
class TokenValidator {
fun validateWithPublicKey(key: PublicKey, token: String): Claims =
fun validate(key: PublicKey, token: String): Claims =
validateToken { Jwts.parser().verifyWith(key).build().parseSignedClaims(token).payload }

fun validateWithSecretKey(key: SecretKey, token: String): Claims =
fun validate(key: SecretKey, token: String): Claims =
validateToken { Jwts.parser().verifyWith(key).build().parseSignedClaims(token).payload }

private inline fun validateToken(validation: () -> Claims): Claims = try {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.myongjiway.core.domain.user

import com.myongjiway.core.domain.error.CoreErrorType
import com.myongjiway.core.domain.error.CoreException
import org.springframework.stereotype.Component

@Component
class UserReader(
private val userRepository: UserRepository,
) {
fun find(id: Long): User? = userRepository.findUserById(id)
fun find(providerId: String): User? = userRepository.findUserByProviderId(providerId)
fun find(id: Long): User = userRepository.findUserById(id) ?: throw CoreException(CoreErrorType.USER_NOT_FOUND)
fun find(providerId: String): User = userRepository.findUserByProviderId(providerId) ?: throw CoreException(CoreErrorType.USER_NOT_FOUND)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.springframework.stereotype.Service

@Service
class UserService(
private val userUpdater: com.myongjiway.core.domain.user.UserUpdater,
private val userUpdater: UserUpdater,
) {
fun inactive(providerId: String): Long = userUpdater.inactive(providerId)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.myongjiway
package com.myongjiway.core.domain

import org.junit.jupiter.api.Tag
import org.springframework.boot.test.context.SpringBootTest
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.myongjiway
package com.myongjiway.core.domain

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
package com.myongjiway.notice
package com.myongjiway.core.domain.notice

import com.myongjiway.core.domain.notice.NoticeCreator
import com.myongjiway.core.domain.notice.NoticeMetadata
import com.myongjiway.core.domain.notice.NoticeRepository
import com.myongjiway.core.domain.notice.NoticeService
import io.kotest.core.spec.style.FeatureSpec
import io.mockk.Runs
import io.mockk.every
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package com.myongjiway.notice
package com.myongjiway.core.domain.notice

import com.myongjiway.core.domain.error.CoreErrorType
import com.myongjiway.core.domain.notice.NoticeRepository
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FeatureSpec
import io.kotest.matchers.shouldBe
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package com.myongjiway.notice
package com.myongjiway.core.domain.notice

import com.myongjiway.core.domain.notice.NoticeFinder
import com.myongjiway.core.domain.notice.NoticeMetadata
import com.myongjiway.core.domain.notice.NoticeRepository
import com.myongjiway.core.domain.notice.NoticeService
import com.myongjiway.core.domain.notice.NoticeView
import com.myongjiway.core.domain.usernotice.UserNotice
import com.myongjiway.core.domain.usernotice.UserNoticeRepository
import io.kotest.core.spec.style.FeatureSpec
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
package com.myongjiway.notice
package com.myongjiway.core.domain.notice

import com.myongjiway.core.domain.notice.NoticeMetadata
import com.myongjiway.core.domain.notice.NoticeRepository
import com.myongjiway.core.domain.notice.NoticeService
import com.myongjiway.core.domain.notice.NoticeUpdater
import com.myongjiway.core.domain.notice.NoticeView
import io.kotest.core.spec.style.FeatureSpec
import io.kotest.matchers.collections.shouldHaveSize
import io.kotest.matchers.shouldBe
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.myongjiway.token
package com.myongjiway.core.domain.token

import com.myongjiway.core.domain.token.TokenAppender
import com.myongjiway.core.domain.token.TokenRepository
import io.kotest.core.spec.style.FeatureSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
Expand Down
Original file line number Diff line number Diff line change
@@ -1,33 +1,36 @@
package com.myongjiway.token
package com.myongjiway.core.domain.token

import com.myongjiway.core.domain.token.JwtProperty
import com.myongjiway.core.domain.token.Token
import com.myongjiway.core.domain.token.TokenGenerator
import com.myongjiway.core.domain.token.TokenType
import io.kotest.core.spec.style.FeatureSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.shouldBeInstanceOf
import io.mockk.mockk
import java.util.Date

class TokenGeneratorTest :
FeatureSpec(
{
lateinit var jwtProperty: JwtProperty
lateinit var tokenValidator: TokenValidator
lateinit var tokenAppender: TokenAppender
lateinit var sut: TokenGenerator
val userId = "1234"

beforeTest {
jwtProperty = JwtProperty().apply {
accessToken = JwtProperty.TokenProperties().apply {
expiration = 10000
secret = "lnp1ISIafo9E+U+xZ4xr0kaRGD5uNVCT1tiJ8gXmqWvp32L7JoXC9EjAy0z2F6NVSwrKLxbCkpzT+DZJazy3Pg=="
secret =
"lnp1ISIafo9E+U+xZ4xr0kaRGD5uNVCT1tiJ8gXmqWvp32L7JoXC9EjAy0z2F6NVSwrKLxbCkpzT+DZJazy3Pg=="
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

원래 secret을 저렇게 하드코딩 하나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HS512 알고리즘을 적용하려면 어느 정도 길이의 key가 필요해서 무작위로 넣었습니다. 나중에 fixture로 빼서 refactoring할 예정입니다

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무작위가 아니라 저희 키값을 넣어놓으셨는데요 ;;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자세히 보면 다릅니다. 자세히 보아야 아름다워요

refreshToken = JwtProperty.TokenProperties().apply {
expiration = 1000000
secret = "lnp1ISIafo9E+U+xZ4xr0kaRGD5uNVCT1tiJ8gXmqWvp32L7JoXC9EjAy0z2F6NVSwrKLxbCkpzT+DZJazy3Pg=="
secret =
"lnp1ISIafo9E+U+xZ4xr0kaRGD5uNVCT1tiJ8gXmqWvp32L7JoXC9EjAy0z2F6NVSwrKLxbCkpzT+DZJazy3Pg=="
}
}
sut = TokenGenerator(jwtProperty)
tokenAppender = mockk()
tokenValidator = mockk()
sut = TokenGenerator(jwtProperty, tokenValidator, tokenAppender)
}

feature("토큰 생성") {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package com.myongjiway.token
package com.myongjiway.core.domain.token

import com.myongjiway.core.domain.token.TokenProcessor
import com.myongjiway.core.domain.token.TokenRepository
import io.kotest.core.spec.style.FeatureSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package com.myongjiway.token
package com.myongjiway.core.domain.token

import com.myongjiway.core.domain.token.Token
import com.myongjiway.core.domain.token.TokenReader
import com.myongjiway.core.domain.token.TokenRepository
import com.myongjiway.core.domain.token.TokenType
import com.myongjiway.core.domain.error.CoreErrorType
import com.myongjiway.core.domain.error.CoreException
import io.kotest.core.spec.style.FeatureSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
Expand Down Expand Up @@ -32,22 +30,22 @@ class TokenReaderTest :
)

// when
val refreshToken = sut.findByToken(token)
val refreshToken = sut.find(token)

// then
refreshToken?.token shouldBe token
refreshToken?.userId shouldBe "1000"
}

scenario("토큰이 존재하지 않으면 null을 반환한다.") {
scenario("토큰이 존재하지 않으면 TOKEN_NOT_FOUND 에러를 반환한다.") {
// given
every { tokenRepository.find("token") } returns null
every { tokenRepository.find("token") } throws CoreException(CoreErrorType.TOKEN_NOT_FOUND)

// when
val refreshToken = sut.findByToken("token")
val actual = kotlin.runCatching { sut.find("token") }

// then
refreshToken shouldBe null
actual.exceptionOrNull() shouldBe CoreException(CoreErrorType.TOKEN_NOT_FOUND)
}
}
},
Expand Down
Loading
Loading