From cf8761f8061dcdb9ac03b46a656c7aaca4516c30 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Fri, 15 Nov 2024 20:25:47 +0900 Subject: [PATCH 01/14] =?UTF-8?q?Feature:=20(BRD-74)=20=EC=8A=A4=ED=94=84?= =?UTF-8?q?=EB=A7=81=20=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EC=B2=B4=EC=9D=B8=20=EA=B8=B0=EB=B3=B8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../external-security/build.gradle.kts | 4 +- .../system/core/config/FilterChainConfig.kt | 60 +++++++++++++++++++ buildSrc/src/main/kotlin/Dependencies.kt | 2 +- 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt diff --git a/board-system-external/external-security/build.gradle.kts b/board-system-external/external-security/build.gradle.kts index 37754532..f8589562 100644 --- a/board-system-external/external-security/build.gradle.kts +++ b/board-system-external/external-security/build.gradle.kts @@ -1,6 +1,6 @@ dependencies { - implementation(Dependencies.SPRING_BOOT_STARTER.fullName) - implementation(Dependencies.SPRING_SECURITY_CRYPTO.fullName) + implementation(Dependencies.SPRING_BOOT_SECURITY.fullName) + implementation(Dependencies.SPRING_BOOT_WEB.fullName) implementation(Dependencies.SPRING_SECURITY_JOSE.fullName) implementation(project(":board-system-domain:domain-core")) implementation(project(":board-system-domain:domain-member")) diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt new file mode 100644 index 00000000..ff2088a8 --- /dev/null +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt @@ -0,0 +1,60 @@ +package com.ttasjwi.board.system.core.config + +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.annotation.Order +import org.springframework.http.HttpMethod +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.web.SecurityFilterChain +import org.springframework.security.config.annotation.web.invoke +import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.savedrequest.NullRequestCache + +@Configuration +class FilterChainConfig { + + @Bean + @Order(0) + fun apiSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + securityMatcher("/api/**") + authorizeHttpRequests { + authorize(HttpMethod.GET, "/api/v1/deploy/health-check", permitAll) + + authorize(HttpMethod.GET, "/api/v1/members/email-available", permitAll) + authorize(HttpMethod.GET, "/api/v1/members/username-available", permitAll) + authorize(HttpMethod.GET, "/api/v1/members/nickname-available", permitAll) + + authorize(HttpMethod.POST, "/api/v1/members/email-verification/start", permitAll) + authorize(HttpMethod.POST, "/api/v1/members/email-verification", permitAll) + authorize(HttpMethod.POST, "/api/v1/members", permitAll) + + authorize(HttpMethod.POST, "/api/v1/auth/login", permitAll) + + authorize(anyRequest, authenticated) + } + + csrf { disable() } + + sessionManagement { + sessionCreationPolicy = SessionCreationPolicy.STATELESS + } + + requestCache { + requestCache = NullRequestCache() + } + } + return http.build() + } + + @Bean + @Order(1) + fun staticResourceSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { + http { + authorizeHttpRequests { + authorize(anyRequest, permitAll) + } + } + return http.build() + } +} diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 839a1ffd..5b527261 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -15,9 +15,9 @@ enum class Dependencies( SPRING_BOOT_WEB(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-web"), SPRING_BOOT_DATA_JPA(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-data-jpa"), SPRING_BOOT_DATA_REDIS(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-data-redis"), + SPRING_BOOT_SECURITY(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-security"), SPRING_BOOT_MAIL(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-mail"), SPRING_BOOT_TEST(groupId = "org.springframework.boot", artifactId = "spring-boot-starter-test"), - SPRING_SECURITY_CRYPTO(groupId = "org.springframework.security", artifactId = "spring-security-crypto"), SPRING_SECURITY_JOSE(groupId = "org.springframework.security", artifactId = "spring-security-oauth2-jose"), // jackson date time From c43195959f649621185685ab78351fe90a9f79d5 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Fri, 15 Nov 2024 22:33:00 +0900 Subject: [PATCH 02/14] =?UTF-8?q?Feature:=20(BRD-74)=20AccessTokenManager?= =?UTF-8?q?=20-=20=EC=95=A1=EC=84=B8=EC=8A=A4=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=ED=98=84=EC=9E=AC=EC=9C=A0=ED=9A=A8=EC=84=B1=20=EC=B2=B4?= =?UTF-8?q?=ED=81=AC=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/AccessTokenExpiredException.kt | 16 +++++ .../system/auth/domain/model/AccessToken.kt | 7 ++ .../auth/domain/service/AccessTokenManager.kt | 1 + .../service/impl/AccessTokenManagerImpl.kt | 4 ++ .../AccessTokenExpiredExceptionTest.kt | 27 ++++++++ .../fixture/AccessTokenManagerFixtureTest.kt | 23 +++++++ .../impl/AccessTokenManagerImplTest.kt | 64 +++++++++++++++++-- .../fixture/AccessTokenManagerFixture.kt | 2 + 8 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessTokenExpiredException.kt create mode 100644 board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessTokenExpiredExceptionTest.kt diff --git a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessTokenExpiredException.kt b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessTokenExpiredException.kt new file mode 100644 index 00000000..e1e79aa4 --- /dev/null +++ b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessTokenExpiredException.kt @@ -0,0 +1,16 @@ +package com.ttasjwi.board.system.auth.domain.exception + +import com.ttasjwi.board.system.core.exception.CustomException +import com.ttasjwi.board.system.core.exception.ErrorStatus +import java.time.ZonedDateTime + +class AccessTokenExpiredException( + expiredAt: ZonedDateTime, + currentTime: ZonedDateTime +) : CustomException( + status = ErrorStatus.UNAUTHENTICATED, + code = "Error.AccessTokenExpired", + args = listOf(expiredAt, currentTime), + source = "accessToken", + debugMessage = "액세스 토큰의 유효시간이 경과되어 더 이상 유효하지 않습니다. (expiredAt=${expiredAt},currentTime=${currentTime}) 리프레시 토큰을 통해 갱신해주세요. 리프레시 토큰도 만료됐다면 로그인을 다시 하셔야합니다." +) diff --git a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/model/AccessToken.kt b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/model/AccessToken.kt index 6c5fec52..69e611d1 100644 --- a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/model/AccessToken.kt +++ b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/model/AccessToken.kt @@ -1,5 +1,6 @@ package com.ttasjwi.board.system.auth.domain.model +import com.ttasjwi.board.system.auth.domain.exception.AccessTokenExpiredException import java.time.ZonedDateTime class AccessToken @@ -50,4 +51,10 @@ internal constructor( result = 31 * result + expiresAt.hashCode() return result } + + fun checkCurrentlyValid(currentTime: ZonedDateTime) { + if (currentTime >= this.expiresAt) { + throw AccessTokenExpiredException(expiredAt = this.expiresAt, currentTime = currentTime) + } + } } diff --git a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/service/AccessTokenManager.kt b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/service/AccessTokenManager.kt index 9995f446..30ab9e29 100644 --- a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/service/AccessTokenManager.kt +++ b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/service/AccessTokenManager.kt @@ -8,4 +8,5 @@ interface AccessTokenManager { fun generate(authMember: AuthMember, issuedAt: ZonedDateTime): AccessToken fun parse(tokenValue: String): AccessToken + fun checkCurrentlyValid(accessToken: AccessToken, currentTime: ZonedDateTime) } diff --git a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/service/impl/AccessTokenManagerImpl.kt b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/service/impl/AccessTokenManagerImpl.kt index bd772f64..714a00bd 100644 --- a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/service/impl/AccessTokenManagerImpl.kt +++ b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/service/impl/AccessTokenManagerImpl.kt @@ -20,4 +20,8 @@ internal class AccessTokenManagerImpl( override fun parse(tokenValue: String): AccessToken { return externalAccessTokenManager.parse(tokenValue) } + + override fun checkCurrentlyValid(accessToken: AccessToken, currentTime: ZonedDateTime) { + accessToken.checkCurrentlyValid(currentTime) + } } diff --git a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessTokenExpiredExceptionTest.kt b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessTokenExpiredExceptionTest.kt new file mode 100644 index 00000000..95ae7bce --- /dev/null +++ b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessTokenExpiredExceptionTest.kt @@ -0,0 +1,27 @@ +package com.ttasjwi.board.system.auth.domain.exception + +import com.ttasjwi.board.system.core.exception.ErrorStatus +import com.ttasjwi.board.system.core.time.fixture.timeFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("AccessTokenExpiredException: 액세스토큰이 만료됐을 때 발생하는 예외") +class AccessTokenExpiredExceptionTest { + + @Test + @DisplayName("예외 기본값 테스트") + fun test1() { + val expiredAt = timeFixture(minute = 3) + val currentTime = timeFixture(minute = 5) + val exception = AccessTokenExpiredException(expiredAt, currentTime) + + assertThat(exception.status).isEqualTo(ErrorStatus.UNAUTHENTICATED) + assertThat(exception.code).isEqualTo("Error.AccessTokenExpired") + assertThat(exception.source).isEqualTo("accessToken") + assertThat(exception.args).containsExactly(expiredAt, currentTime) + assertThat(exception.message).isEqualTo(exception.debugMessage) + assertThat(exception.cause).isNull() + assertThat(exception.debugMessage).isEqualTo("액세스 토큰의 유효시간이 경과되어 더 이상 유효하지 않습니다. (expiredAt=${expiredAt},currentTime=${currentTime}) 리프레시 토큰을 통해 갱신해주세요. 리프레시 토큰도 만료됐다면 로그인을 다시 하셔야합니다.") + } +} diff --git a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixtureTest.kt b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixtureTest.kt index 743d4461..af525f1b 100644 --- a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixtureTest.kt +++ b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixtureTest.kt @@ -1,5 +1,6 @@ package com.ttasjwi.board.system.auth.domain.service.fixture +import com.ttasjwi.board.system.auth.domain.model.fixture.accessTokenFixture import com.ttasjwi.board.system.auth.domain.model.fixture.authMemberFixture import com.ttasjwi.board.system.core.time.fixture.timeFixture import com.ttasjwi.board.system.logging.getLogger @@ -69,4 +70,26 @@ class AccessTokenManagerFixtureTest { assertThat(accessToken.expiresAt).isEqualTo(timeFixture(minute = 35)) } } + + + @Nested + @DisplayName("checkCurrentlyValid: 액세스토큰이 현재 유효한 지 검증한다") + inner class CheckCurrentlyValid { + + + @Test + @DisplayName("픽스쳐에서는 아무 일도 일어나지 않음") + fun test() { + // given + val accessToken = accessTokenFixture( + issuedAt = timeFixture(minute = 0), + expiresAt = timeFixture(minute = 2) + ) + val currentTime = timeFixture(minute = 1) + + // when + // then + accessTokenManagerFixture.checkCurrentlyValid(accessToken, currentTime) + } + } } diff --git a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/impl/AccessTokenManagerImplTest.kt b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/impl/AccessTokenManagerImplTest.kt index d3e9c7ae..145585a0 100644 --- a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/impl/AccessTokenManagerImplTest.kt +++ b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/impl/AccessTokenManagerImplTest.kt @@ -1,16 +1,15 @@ package com.ttasjwi.board.system.auth.domain.service.impl +import com.ttasjwi.board.system.auth.domain.exception.AccessTokenExpiredException import com.ttasjwi.board.system.auth.domain.external.fixture.ExternalAccessTokenManagerFixture import com.ttasjwi.board.system.auth.domain.model.AccessToken +import com.ttasjwi.board.system.auth.domain.model.fixture.accessTokenFixture import com.ttasjwi.board.system.auth.domain.model.fixture.authMemberFixture import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager import com.ttasjwi.board.system.core.time.fixture.timeFixture import com.ttasjwi.board.system.logging.getLogger import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test +import org.junit.jupiter.api.* @DisplayName("AccessTokenManagerImpl 테스트") class AccessTokenManagerImplTest { @@ -76,4 +75,61 @@ class AccessTokenManagerImplTest { } } + @Nested + @DisplayName("checkCurrentlyValid: 액세스토큰이 현재 유효한 지 검증한다") + inner class CheckCurrentlyValid { + + + @Test + @DisplayName("현재 시간이 만료시간 이전 -> 성공") + fun testValid() { + // given + val accessToken = accessTokenFixture( + issuedAt = timeFixture(minute = 0), + expiresAt = timeFixture(minute = 2) + ) + val currentTime = timeFixture(minute = 1) + + // when + // then + accessTokenManager.checkCurrentlyValid(accessToken, currentTime) + } + + @Test + @DisplayName("현재 시간이 만료시간과 같음 -> 예외") + fun testExpired1() { + // given + val accessToken = accessTokenFixture( + issuedAt = timeFixture(minute = 0), + expiresAt = timeFixture(minute = 2) + ) + val currentTime = timeFixture(minute = 2) + + // when + // then + assertThrows { + accessTokenManager.checkCurrentlyValid(accessToken, currentTime) + } + } + + @Test + @DisplayName("현재 시간이 만료시간 이후 -> 예외") + fun testExpired2() { + // given + val accessToken = accessTokenFixture( + issuedAt = timeFixture(minute = 0), + expiresAt = timeFixture(minute = 2) + ) + val currentTime = timeFixture(minute = 3) + + // when + // then + assertThrows { + accessTokenManager.checkCurrentlyValid( + accessToken, + currentTime + ) + } + } + } } diff --git a/board-system-domain/domain-auth/src/testFixtures/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixture.kt b/board-system-domain/domain-auth/src/testFixtures/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixture.kt index a2d38bf6..61ac05bd 100644 --- a/board-system-domain/domain-auth/src/testFixtures/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixture.kt +++ b/board-system-domain/domain-auth/src/testFixtures/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixture.kt @@ -44,6 +44,8 @@ class AccessTokenManagerFixture : AccessTokenManager { ) } + override fun checkCurrentlyValid(accessToken: AccessToken, currentTime: ZonedDateTime) {} + private fun makeTokenValue(authMember: AuthMember, issuedAt: ZonedDateTime, expiresAt: ZonedDateTime): String { return "${authMember.memberId.value}," + // 0 "${authMember.role.name}," + // 1 From 73b703dec4c6d8801ddacc05e2a7105671f40494 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sat, 16 Nov 2024 08:48:18 +0900 Subject: [PATCH 03/14] =?UTF-8?q?Feature:=20(BRD-74)=20BearerTokenResolver?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98(Authorization=20=ED=97=A4=EB=8D=94?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=86=A0=ED=81=B0=EA=B0=92=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spring/security/BearerTokenResolver.kt | 20 ++++++ ...validAuthorizationHeaderFormatException.kt | 12 ++++ .../security/BearerTokenResolverTest.kt | 66 +++++++++++++++++++ ...dAuthorizationHeaderFormatExceptionTest.kt | 24 +++++++ build.gradle.kts | 1 + buildSrc/src/main/kotlin/Dependencies.kt | 4 +- 6 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolver.kt create mode 100644 board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt create mode 100644 board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolverTest.kt create mode 100644 board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolver.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolver.kt new file mode 100644 index 00000000..dc219c2c --- /dev/null +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolver.kt @@ -0,0 +1,20 @@ +package com.ttasjwi.board.system.auth.domain.external.spring.security + +import com.ttasjwi.board.system.auth.domain.external.spring.security.exception.InvalidAuthorizationHeaderFormatException +import jakarta.servlet.http.HttpServletRequest +import org.springframework.http.HttpHeaders + +class BearerTokenResolver { + + private val bearerTokenHeaderName = HttpHeaders.AUTHORIZATION + + fun resolve(request: HttpServletRequest): String? { + val authorizationHeader = request.getHeader(this.bearerTokenHeaderName) ?: return null + + if (!authorizationHeader.startsWith("Bearer ", ignoreCase = true)) { + throw InvalidAuthorizationHeaderFormatException() + } + return authorizationHeader.substring(7) + } + +} diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt new file mode 100644 index 00000000..0f26ec8b --- /dev/null +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt @@ -0,0 +1,12 @@ +package com.ttasjwi.board.system.auth.domain.external.spring.security.exception + +import com.ttasjwi.board.system.core.exception.CustomException +import com.ttasjwi.board.system.core.exception.ErrorStatus + +class InvalidAuthorizationHeaderFormatException : CustomException( + status = ErrorStatus.UNAUTHENTICATED, + code = "Error.InvalidAuthorizationHeaderFormat", + args = emptyList(), + source = "authorizationHeader", + debugMessage = "잘못된 Authorization 헤더 형식입니다. 토큰값을 Authorization 헤더에 'Bearer [토큰값]' 형식으로 보내주세요." +) diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolverTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolverTest.kt new file mode 100644 index 00000000..5f7f01b2 --- /dev/null +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolverTest.kt @@ -0,0 +1,66 @@ +package com.ttasjwi.board.system.auth.domain.external.spring.security + +import com.ttasjwi.board.system.auth.domain.external.spring.security.exception.InvalidAuthorizationHeaderFormatException +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import jakarta.servlet.http.HttpServletRequest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.http.HttpHeaders + +@DisplayName("BearerTokenResolver: Authorization 헤더의 Bearer 뒤에 위치한 토큰값을 분리한다.") +class BearerTokenResolverTest { + private lateinit var bearerTokenResolver: BearerTokenResolver + + @BeforeEach + fun setup() { + bearerTokenResolver = BearerTokenResolver() + } + + @Test + @DisplayName("헤더값이 유효하다면, 토큰값이 성공적으로 분리된다.") + fun testSuccess() { + // given + val request = mockk() + every { request.getHeader(HttpHeaders.AUTHORIZATION) } returns "Bearer validToken123" + + + // when + val token = bearerTokenResolver.resolve(request) + + // then + assertThat(token).isEqualTo("validToken123") + verify { request.getHeader(HttpHeaders.AUTHORIZATION) } + } + + @Test + @DisplayName("Authorization 헤더값이 없을 경우 null 이 반환된다.") + fun authorizationHeaderNull() { + // given + val request = mockk() + every { request.getHeader(HttpHeaders.AUTHORIZATION) } returns null + + // when + val token = bearerTokenResolver.resolve(request) + + // then + assertThat(token).isNull() + } + + @Test + @DisplayName("Authorization 헤더값이 Bearer 로 시작하지 않으면 예외가 발생한다.") + fun testBadAuthorizationHeader() { + // given + val request = mockk() + every { request.getHeader(HttpHeaders.AUTHORIZATION) } returns "Basic abc123" + + // when + // then + assertThrows { bearerTokenResolver.resolve(request) } + verify { request.getHeader(HttpHeaders.AUTHORIZATION) } + } +} diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt new file mode 100644 index 00000000..06f7d7ae --- /dev/null +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt @@ -0,0 +1,24 @@ +package com.ttasjwi.board.system.auth.domain.external.spring.security.exception + +import com.ttasjwi.board.system.core.exception.ErrorStatus +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("InvalidAuthorizationHeaderFormatException: Authorization 헤더값 포맷이 잘못된 형식일 때 발생하는 예외") +class InvalidAuthorizationHeaderFormatExceptionTest { + + @Test + @DisplayName("예외 기본값 테스트") + fun test() { + val exception = InvalidAuthorizationHeaderFormatException() + + assertThat(exception.status).isEqualTo(ErrorStatus.UNAUTHENTICATED) + assertThat(exception.code).isEqualTo("Error.InvalidAuthorizationHeaderFormat") + assertThat(exception.source).isEqualTo("authorizationHeader") + assertThat(exception.args).isEmpty() + assertThat(exception.message).isEqualTo(exception.debugMessage) + assertThat(exception.cause).isNull() + assertThat(exception.debugMessage).isEqualTo("잘못된 Authorization 헤더 형식입니다. 토큰값을 Authorization 헤더에 'Bearer [토큰값]' 형식으로 보내주세요.") + } +} diff --git a/build.gradle.kts b/build.gradle.kts index 2bf7848e..84e1afae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -42,6 +42,7 @@ subprojects { implementation(Dependencies.KOTLIN_REFLECT.fullName) testImplementation(Dependencies.SPRING_BOOT_TEST.fullName) + testImplementation(Dependencies.MOCKK.fullName) } tasks.getByName("bootJar") { diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 5b527261..c69c0765 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -33,8 +33,10 @@ enum class Dependencies( YAML_RESOURCE_BUNDLE(groupId = "dev.akkinoc.util", artifactId = "yaml-resource-bundle", version = "2.13.0"), // email-format-check - COMMONS_VALIDATOR(groupId="commons-validator", artifactId ="commons-validator" , version="1.9.0"); + COMMONS_VALIDATOR(groupId="commons-validator", artifactId ="commons-validator" , version="1.9.0"), + // mockk + MOCKK(groupId="io.mockk", artifactId="mockk" , version="1.13.13"); val fullName: String get() { From 791bfda93a25a13286b80a23c7c6d608ecb32e8f Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sat, 16 Nov 2024 15:02:45 +0900 Subject: [PATCH 04/14] =?UTF-8?q?Feature:=20(BRD-74)=20AccessTokenManagerF?= =?UTF-8?q?ixture=20=EA=B5=AC=ED=98=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../fixture/AccessTokenManagerFixtureTest.kt | 48 ++++++++++++++++--- .../fixture/AccessTokenManagerFixture.kt | 10 +++- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixtureTest.kt b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixtureTest.kt index af525f1b..46875a47 100644 --- a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixtureTest.kt +++ b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixtureTest.kt @@ -1,14 +1,12 @@ package com.ttasjwi.board.system.auth.domain.service.fixture +import com.ttasjwi.board.system.auth.domain.exception.AccessTokenExpiredException import com.ttasjwi.board.system.auth.domain.model.fixture.accessTokenFixture import com.ttasjwi.board.system.auth.domain.model.fixture.authMemberFixture import com.ttasjwi.board.system.core.time.fixture.timeFixture import com.ttasjwi.board.system.logging.getLogger import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test +import org.junit.jupiter.api.* @DisplayName("AccessTokenManager 픽스쳐 테스트") class AccessTokenManagerFixtureTest { @@ -76,10 +74,9 @@ class AccessTokenManagerFixtureTest { @DisplayName("checkCurrentlyValid: 액세스토큰이 현재 유효한 지 검증한다") inner class CheckCurrentlyValid { - @Test - @DisplayName("픽스쳐에서는 아무 일도 일어나지 않음") - fun test() { + @DisplayName("현재 시간이 만료시간 이전 -> 성공") + fun testValid() { // given val accessToken = accessTokenFixture( issuedAt = timeFixture(minute = 0), @@ -91,5 +88,42 @@ class AccessTokenManagerFixtureTest { // then accessTokenManagerFixture.checkCurrentlyValid(accessToken, currentTime) } + + @Test + @DisplayName("현재 시간이 만료시간과 같음 -> 예외") + fun testExpired1() { + // given + val accessToken = accessTokenFixture( + issuedAt = timeFixture(minute = 0), + expiresAt = timeFixture(minute = 2) + ) + val currentTime = timeFixture(minute = 2) + + // when + // then + assertThrows { + accessTokenManagerFixture.checkCurrentlyValid(accessToken, currentTime) + } + } + + @Test + @DisplayName("현재 시간이 만료시간 이후 -> 예외") + fun testExpired2() { + // given + val accessToken = accessTokenFixture( + issuedAt = timeFixture(minute = 0), + expiresAt = timeFixture(minute = 2) + ) + val currentTime = timeFixture(minute = 3) + + // when + // then + assertThrows { + accessTokenManagerFixture.checkCurrentlyValid( + accessToken, + currentTime + ) + } + } } } diff --git a/board-system-domain/domain-auth/src/testFixtures/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixture.kt b/board-system-domain/domain-auth/src/testFixtures/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixture.kt index 61ac05bd..b9616646 100644 --- a/board-system-domain/domain-auth/src/testFixtures/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixture.kt +++ b/board-system-domain/domain-auth/src/testFixtures/kotlin/com/ttasjwi/board/system/auth/domain/service/fixture/AccessTokenManagerFixture.kt @@ -1,9 +1,11 @@ package com.ttasjwi.board.system.auth.domain.service.fixture +import com.ttasjwi.board.system.auth.domain.exception.AccessTokenExpiredException import com.ttasjwi.board.system.auth.domain.model.AccessToken import com.ttasjwi.board.system.auth.domain.model.AuthMember import com.ttasjwi.board.system.auth.domain.model.fixture.accessTokenFixture import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager +import com.ttasjwi.board.system.core.time.fixture.timeFixture import com.ttasjwi.board.system.member.domain.model.Role import java.time.ZonedDateTime @@ -18,6 +20,8 @@ class AccessTokenManagerFixture : AccessTokenManager { private const val ISSUED_AT_INDEX = 2 private const val EXPIRES_AT_INDEX = 3 private const val TOKEN_TYPE = "accessToken" + + val ERROR_CHECK_TIME = timeFixture(minute = 37) } override fun generate(authMember: AuthMember, issuedAt: ZonedDateTime): AccessToken { @@ -44,7 +48,11 @@ class AccessTokenManagerFixture : AccessTokenManager { ) } - override fun checkCurrentlyValid(accessToken: AccessToken, currentTime: ZonedDateTime) {} + override fun checkCurrentlyValid(accessToken: AccessToken, currentTime: ZonedDateTime) { + if (accessToken.expiresAt <= currentTime) { + throw AccessTokenExpiredException(accessToken.expiresAt, currentTime) + } + } private fun makeTokenValue(authMember: AuthMember, issuedAt: ZonedDateTime, expiresAt: ZonedDateTime): String { return "${authMember.memberId.value}," + // 0 From ea2c6f5ac6231b04a1e424f69cc75e2c5c7961fb Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sat, 16 Nov 2024 15:25:47 +0900 Subject: [PATCH 05/14] =?UTF-8?q?Feature:=20(BRD-74)=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=ED=9A=8C=EC=9B=90=EC=97=90=20=EB=8C=80=EC=9D=91=ED=95=98?= =?UTF-8?q?=EB=8A=94=20Authentication(Spring=20Security)=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/AuthMemberAuthentication.kt | 46 ++++++++ .../security/AuthMemberAuthenticationTest.kt | 105 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthentication.kt create mode 100644 board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthenticationTest.kt diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthentication.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthentication.kt new file mode 100644 index 00000000..408fc34b --- /dev/null +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthentication.kt @@ -0,0 +1,46 @@ +package com.ttasjwi.board.system.auth.domain.external.spring.security + +import com.ttasjwi.board.system.auth.domain.model.AuthMember +import org.springframework.security.core.Authentication +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority + +class AuthMemberAuthentication +private constructor( + private val authMember: AuthMember +) : Authentication { + + companion object { + fun from(authMember: AuthMember): AuthMemberAuthentication { + return AuthMemberAuthentication(authMember) + } + } + + override fun getName(): String? { + return null + } + + override fun getAuthorities(): MutableCollection { + return mutableListOf(SimpleGrantedAuthority("ROLE_${authMember.role.name}")) + } + + override fun getCredentials(): Any? { + return null + } + + override fun getDetails(): Any? { + return null + } + + override fun getPrincipal(): Any { + return authMember + } + + override fun isAuthenticated(): Boolean { + return true + } + + override fun setAuthenticated(isAuthenticated: Boolean) { + throw IllegalStateException("cannot set this token to trusted") + } +} diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthenticationTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthenticationTest.kt new file mode 100644 index 00000000..6f984a1b --- /dev/null +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthenticationTest.kt @@ -0,0 +1,105 @@ +package com.ttasjwi.board.system.auth.domain.external.spring.security + +import com.ttasjwi.board.system.auth.domain.model.AuthMember +import com.ttasjwi.board.system.auth.domain.model.fixture.authMemberFixture +import com.ttasjwi.board.system.member.domain.model.Role +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.SimpleGrantedAuthority + +@DisplayName("AuthMemberAuthentication: AuthMember를 통해 얻어낸 Spring Security Authentication") +class AuthMemberAuthenticationTest { + + private lateinit var authMember: AuthMember + private lateinit var authentication: Authentication + + @BeforeEach + fun setup() { + authMember = authMemberFixture(memberId = 1357L, role = Role.ADMIN) + authentication = AuthMemberAuthentication.from(authMember) + } + + @Test + @DisplayName("getName: Null 반환") + fun testGetName() { + // given + // when + val name = authentication.name + + // then + assertThat(name).isNull() + } + + @Test + @DisplayName("getAuthorities: 권한목록 반환") + fun testGetAuthorities() { + // given + // when + val authorities = authentication.authorities + + // then + assertThat(authorities).containsExactly(SimpleGrantedAuthority("ROLE_ADMIN")) + } + + @Test + @DisplayName("getCredentials: Null 반환") + fun testGetCredentials() { + // given + // when + val credentials = authentication.credentials + + // then + assertThat(credentials).isNull() + } + + @Test + @DisplayName("getDetails: Null 반환") + fun testGetDetails() { + // given + // when + val details = authentication.details + + // then + assertThat(details).isNull() + } + + @Test + @DisplayName("getPrincipal: authMember 반환") + fun testGetPrincipal() { + // given + // when + val innerPrincipal = authentication.principal as AuthMember + + // then + assertThat(innerPrincipal).isEqualTo(authMember) + } + + + @Test + @DisplayName("isAuthenticated: true 반환") + fun testIsAuthenticated() { + // given + // when + val authenticated = authentication.isAuthenticated + + // then + assertThat(authenticated).isTrue() + } + + + @Test + @DisplayName("setAuthenticated: 예외 발생") + fun testSetAuthenticated() { + // given + // when + // then + assertThrows { authentication.isAuthenticated = true } + .let { + assertThat(it.message).isEqualTo("cannot set this token to trusted") + } + } +} From 9e047ae93f0adb7d7f58816d401a668fc2bddfda Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 08:50:33 +0900 Subject: [PATCH 06/14] =?UTF-8?q?Feature:=20(BRD-74)=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=ED=81=B0=20=EC=9D=B8=EC=A6=9D=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AccessTokenAuthenticationFilter.kt | 66 ++++++++++ .../AccessTokenAuthenticationFilterTest.kt | 117 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilter.kt create mode 100644 board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilterTest.kt diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilter.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilter.kt new file mode 100644 index 00000000..01c8def0 --- /dev/null +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilter.kt @@ -0,0 +1,66 @@ +package com.ttasjwi.board.system.auth.domain.external.spring.security + +import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager +import com.ttasjwi.board.system.core.time.TimeManager +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.filter.OncePerRequestFilter + +class AccessTokenAuthenticationFilter( + private val bearerTokenResolver: BearerTokenResolver, + private val accessTokenManager: AccessTokenManager, + private val timeManager: TimeManager, +) : OncePerRequestFilter() { + + public override fun doFilterInternal( + request: HttpServletRequest, + response: HttpServletResponse, + filterChain: FilterChain + ) { + + // 이미 인증됐다면 통과 + if (isAuthenticated()) { + filterChain.doFilter(request, response) + return + } + + // 헤더를 통해 토큰을 가져옴. 없다면 통과 + val tokenValue = bearerTokenResolver.resolve(request) + if (tokenValue == null) { + filterChain.doFilter(request, response) + return + } + + // 토큰값을 통해 인증 + val authentication = attemptAuthenticate(tokenValue) + + // 인증 결과를 SecurityContextHolder 에 저장 + saveAuthenticationToSecurityContextHolder(authentication) + + // 통과 + try { + filterChain.doFilter(request, response) + } finally { + SecurityContextHolder.getContextHolderStrategy().clearContext() + } + } + + private fun isAuthenticated() = SecurityContextHolder.getContextHolderStrategy().context.authentication != null + + private fun attemptAuthenticate(tokenValue: String): AuthMemberAuthentication { + val accessToken = accessTokenManager.parse(tokenValue) + val currentTime = timeManager.now() + + accessTokenManager.checkCurrentlyValid(accessToken, currentTime) + + return AuthMemberAuthentication.from(accessToken.authMember) + } + + private fun saveAuthenticationToSecurityContextHolder(authentication: AuthMemberAuthentication) { + val securityContext = SecurityContextHolder.getContextHolderStrategy().createEmptyContext() + securityContext.authentication = authentication + SecurityContextHolder.getContextHolderStrategy().context = securityContext + } +} diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilterTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilterTest.kt new file mode 100644 index 00000000..d0da0b3c --- /dev/null +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilterTest.kt @@ -0,0 +1,117 @@ +package com.ttasjwi.board.system.auth.domain.external.spring.security + +import com.ttasjwi.board.system.auth.domain.model.fixture.accessTokenFixture +import com.ttasjwi.board.system.auth.domain.model.fixture.authMemberFixture +import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager +import com.ttasjwi.board.system.core.time.TimeManager +import com.ttasjwi.board.system.core.time.fixture.timeFixture +import com.ttasjwi.board.system.member.domain.model.Role +import io.mockk.* +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.context.SecurityContextHolder + +@DisplayName("AccessTokenAuthenticationFilter 테스트") +class AccessTokenAuthenticationFilterTest { + + private lateinit var accessTokenAuthenticationFilter: AccessTokenAuthenticationFilter + private lateinit var bearerTokenResolver: BearerTokenResolver + private lateinit var accessTokenManager: AccessTokenManager + private lateinit var timeManager: TimeManager + private lateinit var request: HttpServletRequest + private lateinit var response: HttpServletResponse + private lateinit var filterChain: FilterChain + + @BeforeEach + fun setup() { + bearerTokenResolver = mockk() + accessTokenManager = mockk() + timeManager = mockk() + accessTokenAuthenticationFilter = AccessTokenAuthenticationFilter( + bearerTokenResolver = bearerTokenResolver, + accessTokenManager = accessTokenManager, + timeManager = timeManager + ) + SecurityContextHolder.clearContext() + request = mockk() + response = mockk() + filterChain = mockk() + } + + @AfterEach + fun teardown() { + SecurityContextHolder.clearContext() + } + + @Test + @DisplayName("이미 인증된 회원은 필터를 통과시킨다.") + fun testAuthenticated() { + // given + SecurityContextHolder.getContext().authentication = UsernamePasswordAuthenticationToken.authenticated( + authMemberFixture(1L, Role.USER), + "", + mutableListOf(SimpleGrantedAuthority("ROLE_USER")) + ) + + every { filterChain.doFilter(request, response) } just Runs + + // when + accessTokenAuthenticationFilter.doFilterInternal(request, response, filterChain) + + // then + verify(exactly = 1) { filterChain.doFilter(request, response) } + verify(exactly = 0) { bearerTokenResolver.resolve(request) } + } + + @Test + @DisplayName("토큰값이 없다면 다음 필터로 통과시킨다") + fun testNullTokenValue() { + // given + every { bearerTokenResolver.resolve(request) } returns null + every { filterChain.doFilter(request, response) } just Runs + + // when + accessTokenAuthenticationFilter.doFilterInternal(request, response, filterChain) + + // then + verify(exactly = 1) { bearerTokenResolver.resolve(request) } + verify(exactly = 1) { filterChain.doFilter(request, response) } + verify(exactly = 0) { accessTokenManager.parse(anyNullable()) } + } + + @Test + @DisplayName("인증 토큰을 지참했다면 인증을 해야 통과된다.") + fun testWithValidAccessToken() { + // given + val tokenValue = "validToken" + val accessToken = accessTokenFixture( + memberId = 1557L, role = Role.ADMIN, tokenValue = tokenValue, + issuedAt = timeFixture(minute = 5), expiresAt = timeFixture(minute = 35) + ) + val currentTime = timeFixture(minute = 13) + + every { bearerTokenResolver.resolve(request) } returns tokenValue + every { accessTokenManager.parse(tokenValue) } returns accessToken + every { timeManager.now() } returns currentTime + every { accessTokenManager.checkCurrentlyValid(accessToken, currentTime) } just Runs + every { filterChain.doFilter(request, response) } just Runs + + // when + accessTokenAuthenticationFilter.doFilterInternal(request, response, filterChain) + + // then + verify(exactly = 1) { accessTokenManager.parse(tokenValue) } + verify(exactly = 1) { bearerTokenResolver.resolve(request) } + verify(exactly = 1) { timeManager.now() } + verify(exactly = 1) { accessTokenManager.checkCurrentlyValid(accessToken, currentTime) } + verify(exactly = 1) { filterChain.doFilter(request, response) } + } + +} From c83517b18878fcc18c44eef4816345c7d1a2923b Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 08:56:05 +0900 Subject: [PATCH 07/14] =?UTF-8?q?Feature:=20(BRD-74)=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=ED=86=A0=ED=81=B0=20=EC=9D=B8=EC=A6=9D=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=ED=95=84=ED=84=B0=EC=B2=B4=EC=9D=B8?= =?UTF-8?q?=EC=97=90=20=ED=8F=AC=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../system/core/config/FilterChainConfig.kt | 20 ++++- .../ttasjwi/board/system/MvcTestController.kt | 17 ++++ .../board/system/MvcTestControllerTest.kt | 79 +++++++++++++++++++ .../board/system/SecurityTestApplication.kt | 16 +++- 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/MvcTestController.kt create mode 100644 board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/MvcTestControllerTest.kt diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt index ff2088a8..6a295fa5 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt @@ -1,5 +1,9 @@ package com.ttasjwi.board.system.core.config +import com.ttasjwi.board.system.auth.domain.external.spring.security.AccessTokenAuthenticationFilter +import com.ttasjwi.board.system.auth.domain.external.spring.security.BearerTokenResolver +import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager +import com.ttasjwi.board.system.core.time.TimeManager import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.annotation.Order @@ -8,10 +12,15 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.web.SecurityFilterChain import org.springframework.security.config.annotation.web.invoke import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.savedrequest.NullRequestCache +import org.springframework.web.filter.OncePerRequestFilter @Configuration -class FilterChainConfig { +class FilterChainConfig( + private val accessTokenManager: AccessTokenManager, + private val timeManager: TimeManager, +) { @Bean @Order(0) @@ -43,10 +52,19 @@ class FilterChainConfig { requestCache { requestCache = NullRequestCache() } + addFilterBefore(accessTokenAuthenticationFilter()) } return http.build() } + private fun accessTokenAuthenticationFilter(): OncePerRequestFilter { + return AccessTokenAuthenticationFilter( + bearerTokenResolver = BearerTokenResolver(), + accessTokenManager = accessTokenManager, + timeManager = timeManager, + ) + } + @Bean @Order(1) fun staticResourceSecurityFilterChain(http: HttpSecurity): SecurityFilterChain { diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/MvcTestController.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/MvcTestController.kt new file mode 100644 index 00000000..6ae24888 --- /dev/null +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/MvcTestController.kt @@ -0,0 +1,17 @@ +package com.ttasjwi.board.system + +import com.ttasjwi.board.system.auth.domain.model.AuthMember +import org.springframework.http.ResponseEntity +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +class MvcTestController { + + @GetMapping("/api/v1/test/authenticated") + fun testAuthenticated(): ResponseEntity { + val authMember = SecurityContextHolder.getContext().authentication.principal as AuthMember + return ResponseEntity.ok(authMember) + } +} diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/MvcTestControllerTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/MvcTestControllerTest.kt new file mode 100644 index 00000000..176178ab --- /dev/null +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/MvcTestControllerTest.kt @@ -0,0 +1,79 @@ +package com.ttasjwi.board.system + +import com.nimbusds.jose.util.StandardCharset +import com.ttasjwi.board.system.auth.domain.exception.AccessTokenExpiredException +import com.ttasjwi.board.system.auth.domain.model.fixture.authMemberFixture +import com.ttasjwi.board.system.auth.domain.service.fixture.AccessTokenManagerFixture +import com.ttasjwi.board.system.core.time.fixture.TimeManagerFixture +import com.ttasjwi.board.system.core.time.fixture.timeFixture +import com.ttasjwi.board.system.member.domain.model.Role +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* + +@SpringBootTest +@AutoConfigureMockMvc +@DisplayName("MVC 통합 인증/인가 테스트") +class MvcTestControllerTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var accessTokenManagerFixture: AccessTokenManagerFixture + + @Autowired + private lateinit var timeManagerFixture: TimeManagerFixture + + @Test + @DisplayName("유효한 시간 내에 액세스토큰을 전달하면 필터를 통과한다.") + fun testAuthenticated() { + // given + val authMember = authMemberFixture(memberId = 5544, role = Role.USER) + val accessToken = accessTokenManagerFixture.generate(authMember, timeFixture(minute = 5)) + timeManagerFixture.changeCurrentTime(timeFixture(minute = 18)) + + mockMvc + .perform( + get("/api/v1/test/authenticated") + .header(HttpHeaders.AUTHORIZATION, "Bearer ${accessToken.tokenValue}") + .characterEncoding(StandardCharset.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + ) + .andDo(print()) + .andExpectAll( + status().isOk, + content().contentType(MediaType.APPLICATION_JSON), + jsonPath("$.memberId.value").value(authMember.memberId.value), + jsonPath("$.role").value(authMember.role.name) + ) + } + + @Test + @DisplayName("유효시간을 경과하면 예외가 발생하고, 인증에 실패한다.") + fun testFailed() { + // given + val authMember = authMemberFixture(memberId = 5544, role = Role.USER) + val accessToken = accessTokenManagerFixture.generate(authMember, timeFixture(minute = 5)) + timeManagerFixture.changeCurrentTime(timeFixture(minute = 45)) + + assertThrows { + mockMvc + .perform( + get("/api/v1/test/authenticated") + .header(HttpHeaders.AUTHORIZATION, "Bearer ${accessToken.tokenValue}") + .characterEncoding(StandardCharset.UTF_8) + .contentType(MediaType.APPLICATION_JSON) + ) + } + } +} diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/SecurityTestApplication.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/SecurityTestApplication.kt index 991c374d..710f6c1b 100644 --- a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/SecurityTestApplication.kt +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/SecurityTestApplication.kt @@ -1,12 +1,26 @@ package com.ttasjwi.board.system +import com.ttasjwi.board.system.auth.domain.service.fixture.AccessTokenManagerFixture +import com.ttasjwi.board.system.core.time.fixture.TimeManagerFixture import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.context.properties.ConfigurationPropertiesScan import org.springframework.boot.runApplication +import org.springframework.context.annotation.Bean @ConfigurationPropertiesScan @SpringBootApplication -class SecurityTestApplication +class SecurityTestApplication { + + @Bean + fun accessTokenManagerFixture(): AccessTokenManagerFixture { + return AccessTokenManagerFixture() + } + + @Bean + fun timeManagerFixture(): TimeManagerFixture { + return TimeManagerFixture() + } +} fun main(args: Array) { runApplication(*args) From 100401ac48342cf29e3d2aadb06d70363f894967 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 09:10:01 +0900 Subject: [PATCH 08/14] =?UTF-8?q?Chore:=20(BRD-74)=20external-security=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EB=82=B4=20=ED=8C=A8=ED=82=A4=EC=A7=80=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ttasjwi/board/system/core/config/FilterChainConfig.kt | 4 ++-- .../com/ttasjwi/board/system/core/config/NimbusJwtConfig.kt | 2 +- .../authentication}/AccessTokenAuthenticationFilter.kt | 3 ++- .../security/authentication}/AuthMemberAuthentication.kt | 2 +- .../exception/InvalidAuthorizationHeaderFormatException.kt | 2 +- .../spring/security/support}/BearerTokenResolver.kt | 4 ++-- .../security/support}/X509CertificateThumbprintValidator.kt | 2 +- .../authentication}/AccessTokenAuthenticationFilterTest.kt | 3 ++- .../security/authentication}/AuthMemberAuthenticationTest.kt | 2 +- .../InvalidAuthorizationHeaderFormatExceptionTest.kt | 3 ++- .../spring/security/support}/BearerTokenResolverTest.kt | 4 ++-- 11 files changed, 17 insertions(+), 14 deletions(-) rename board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/{auth/domain/external/spring/security => external/spring/security/authentication}/AccessTokenAuthenticationFilter.kt (93%) rename board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/{auth/domain/external/spring/security => external/spring/security/authentication}/AuthMemberAuthentication.kt (94%) rename board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/{auth/domain => }/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt (86%) rename board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/{auth/domain/external/spring/security => external/spring/security/support}/BearerTokenResolver.kt (74%) rename board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/{auth/domain/external/spring/security => external/spring/security/support}/X509CertificateThumbprintValidator.kt (98%) rename board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/{auth/domain/external/spring/security => external/spring/security/authentication}/AccessTokenAuthenticationFilterTest.kt (96%) rename board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/{auth/domain/external/spring/security => external/spring/security/authentication}/AuthMemberAuthenticationTest.kt (97%) rename board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/{auth/domain => }/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt (86%) rename board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/{auth/domain/external/spring/security => external/spring/security/support}/BearerTokenResolverTest.kt (91%) diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt index 6a295fa5..ab940cae 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt @@ -1,7 +1,7 @@ package com.ttasjwi.board.system.core.config -import com.ttasjwi.board.system.auth.domain.external.spring.security.AccessTokenAuthenticationFilter -import com.ttasjwi.board.system.auth.domain.external.spring.security.BearerTokenResolver +import com.ttasjwi.board.system.external.spring.security.authentication.AccessTokenAuthenticationFilter +import com.ttasjwi.board.system.external.spring.security.support.BearerTokenResolver import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager import com.ttasjwi.board.system.core.time.TimeManager import org.springframework.context.annotation.Bean diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/NimbusJwtConfig.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/NimbusJwtConfig.kt index 008cabad..44213bd2 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/NimbusJwtConfig.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/NimbusJwtConfig.kt @@ -12,7 +12,7 @@ import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.proc.ConfigurableJWTProcessor import com.nimbusds.jwt.proc.DefaultJWTProcessor import com.nimbusds.jwt.proc.JWTClaimsSetVerifier -import com.ttasjwi.board.system.auth.domain.external.spring.security.X509CertificateThumbprintValidator +import com.ttasjwi.board.system.external.spring.security.support.X509CertificateThumbprintValidator import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.io.ResourceLoader diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilter.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AccessTokenAuthenticationFilter.kt similarity index 93% rename from board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilter.kt rename to board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AccessTokenAuthenticationFilter.kt index 01c8def0..ed7a078f 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilter.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AccessTokenAuthenticationFilter.kt @@ -1,7 +1,8 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security +package com.ttasjwi.board.system.external.spring.security.authentication import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager import com.ttasjwi.board.system.core.time.TimeManager +import com.ttasjwi.board.system.external.spring.security.support.BearerTokenResolver import jakarta.servlet.FilterChain import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthentication.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AuthMemberAuthentication.kt similarity index 94% rename from board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthentication.kt rename to board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AuthMemberAuthentication.kt index 408fc34b..4f11812f 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthentication.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AuthMemberAuthentication.kt @@ -1,4 +1,4 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security +package com.ttasjwi.board.system.external.spring.security.authentication import com.ttasjwi.board.system.auth.domain.model.AuthMember import org.springframework.security.core.Authentication diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt similarity index 86% rename from board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt rename to board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt index 0f26ec8b..aa8f1a79 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/InvalidAuthorizationHeaderFormatException.kt @@ -1,4 +1,4 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security.exception +package com.ttasjwi.board.system.external.spring.security.exception import com.ttasjwi.board.system.core.exception.CustomException import com.ttasjwi.board.system.core.exception.ErrorStatus diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolver.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/support/BearerTokenResolver.kt similarity index 74% rename from board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolver.kt rename to board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/support/BearerTokenResolver.kt index dc219c2c..b9f7f120 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolver.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/support/BearerTokenResolver.kt @@ -1,6 +1,6 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security +package com.ttasjwi.board.system.external.spring.security.support -import com.ttasjwi.board.system.auth.domain.external.spring.security.exception.InvalidAuthorizationHeaderFormatException +import com.ttasjwi.board.system.external.spring.security.exception.InvalidAuthorizationHeaderFormatException import jakarta.servlet.http.HttpServletRequest import org.springframework.http.HttpHeaders diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/X509CertificateThumbprintValidator.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/support/X509CertificateThumbprintValidator.kt similarity index 98% rename from board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/X509CertificateThumbprintValidator.kt rename to board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/support/X509CertificateThumbprintValidator.kt index cb19ea14..21afb0e2 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/X509CertificateThumbprintValidator.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/support/X509CertificateThumbprintValidator.kt @@ -1,4 +1,4 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security +package com.ttasjwi.board.system.external.spring.security.support import org.apache.commons.logging.Log import org.apache.commons.logging.LogFactory diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilterTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AccessTokenAuthenticationFilterTest.kt similarity index 96% rename from board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilterTest.kt rename to board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AccessTokenAuthenticationFilterTest.kt index d0da0b3c..885d480f 100644 --- a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AccessTokenAuthenticationFilterTest.kt +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AccessTokenAuthenticationFilterTest.kt @@ -1,10 +1,11 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security +package com.ttasjwi.board.system.external.spring.security.authentication import com.ttasjwi.board.system.auth.domain.model.fixture.accessTokenFixture import com.ttasjwi.board.system.auth.domain.model.fixture.authMemberFixture import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager import com.ttasjwi.board.system.core.time.TimeManager import com.ttasjwi.board.system.core.time.fixture.timeFixture +import com.ttasjwi.board.system.external.spring.security.support.BearerTokenResolver import com.ttasjwi.board.system.member.domain.model.Role import io.mockk.* import jakarta.servlet.FilterChain diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthenticationTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AuthMemberAuthenticationTest.kt similarity index 97% rename from board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthenticationTest.kt rename to board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AuthMemberAuthenticationTest.kt index 6f984a1b..2013002a 100644 --- a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/AuthMemberAuthenticationTest.kt +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/authentication/AuthMemberAuthenticationTest.kt @@ -1,4 +1,4 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security +package com.ttasjwi.board.system.external.spring.security.authentication import com.ttasjwi.board.system.auth.domain.model.AuthMember import com.ttasjwi.board.system.auth.domain.model.fixture.authMemberFixture diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt similarity index 86% rename from board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt rename to board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt index 06f7d7ae..2363fdbe 100644 --- a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/InvalidAuthorizationHeaderFormatExceptionTest.kt @@ -1,5 +1,6 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security.exception +package com.ttasjwi.board.system.external.spring.security.exception +import com.ttasjwi.board.system.external.spring.security.exception.InvalidAuthorizationHeaderFormatException import com.ttasjwi.board.system.core.exception.ErrorStatus import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.DisplayName diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolverTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/support/BearerTokenResolverTest.kt similarity index 91% rename from board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolverTest.kt rename to board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/support/BearerTokenResolverTest.kt index 5f7f01b2..1d72c9d0 100644 --- a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/auth/domain/external/spring/security/BearerTokenResolverTest.kt +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/support/BearerTokenResolverTest.kt @@ -1,6 +1,6 @@ -package com.ttasjwi.board.system.auth.domain.external.spring.security +package com.ttasjwi.board.system.external.spring.security.support -import com.ttasjwi.board.system.auth.domain.external.spring.security.exception.InvalidAuthorizationHeaderFormatException +import com.ttasjwi.board.system.external.spring.security.exception.InvalidAuthorizationHeaderFormatException import io.mockk.every import io.mockk.mockk import io.mockk.verify From d03a28eeee627b961a0a14359337ee339d739121 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 10:11:43 +0900 Subject: [PATCH 09/14] =?UTF-8?q?Feature:=20(BRD-74)=20=EC=BB=A4=EC=8A=A4?= =?UTF-8?q?=ED=85=80=20AuthenticationEntryPoint,=20AccessDeniedHandler=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthorizationFilter에서 발생한 예외는 그 앞에 위치한 ExceptionTranslationFilter로 전파됩니다. - 여기서 인증예외/인가예외는 AuthenticationEntryPoint, AccessDeniedHandler로 전달되어 처리됩니다 - 이 부분을 커스터마이징하였습니다. --- .../system/core/config/FilterChainConfig.kt | 12 +++++ .../exception/CustomAccessDeniedHandler.kt | 20 +++++++ .../CustomAuthenticationEntryPoint.kt | 20 +++++++ .../CustomAccessDeniedHandlerTest.kt | 50 ++++++++++++++++++ .../CustomAuthenticationEntryPointTest.kt | 52 +++++++++++++++++++ 5 files changed, 154 insertions(+) create mode 100644 board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandler.kt create mode 100644 board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPoint.kt create mode 100644 board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandlerTest.kt create mode 100644 board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPointTest.kt diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt index ab940cae..e60580fd 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/core/config/FilterChainConfig.kt @@ -4,6 +4,9 @@ import com.ttasjwi.board.system.external.spring.security.authentication.AccessTo import com.ttasjwi.board.system.external.spring.security.support.BearerTokenResolver import com.ttasjwi.board.system.auth.domain.service.AccessTokenManager import com.ttasjwi.board.system.core.time.TimeManager +import com.ttasjwi.board.system.external.spring.security.exception.CustomAccessDeniedHandler +import com.ttasjwi.board.system.external.spring.security.exception.CustomAuthenticationEntryPoint +import org.springframework.beans.factory.annotation.Qualifier import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.core.annotation.Order @@ -15,11 +18,14 @@ import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.springframework.security.web.savedrequest.NullRequestCache import org.springframework.web.filter.OncePerRequestFilter +import org.springframework.web.servlet.HandlerExceptionResolver @Configuration class FilterChainConfig( private val accessTokenManager: AccessTokenManager, private val timeManager: TimeManager, + @Qualifier(value = "handlerExceptionResolver") + private val handlerExceptionResolver: HandlerExceptionResolver, ) { @Bean @@ -52,6 +58,12 @@ class FilterChainConfig( requestCache { requestCache = NullRequestCache() } + + exceptionHandling { + authenticationEntryPoint = CustomAuthenticationEntryPoint(handlerExceptionResolver) + accessDeniedHandler = CustomAccessDeniedHandler(handlerExceptionResolver) + } + addFilterBefore(accessTokenAuthenticationFilter()) } return http.build() diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandler.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandler.kt new file mode 100644 index 00000000..eb9a6379 --- /dev/null +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandler.kt @@ -0,0 +1,20 @@ +package com.ttasjwi.board.system.external.spring.security.exception + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.web.servlet.HandlerExceptionResolver + +class CustomAccessDeniedHandler( + private val handlerExceptionResolver: HandlerExceptionResolver +) : AccessDeniedHandler { + + override fun handle( + request: HttpServletRequest, + response: HttpServletResponse, + accessDeniedException: AccessDeniedException + ) { + handlerExceptionResolver.resolveException(request, response, null, accessDeniedException) + } +} diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPoint.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPoint.kt new file mode 100644 index 00000000..d6ef27e0 --- /dev/null +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPoint.kt @@ -0,0 +1,20 @@ +package com.ttasjwi.board.system.external.spring.security.exception + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.core.AuthenticationException +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.web.servlet.HandlerExceptionResolver + +class CustomAuthenticationEntryPoint( + private val handlerExceptionResolver: HandlerExceptionResolver +) : AuthenticationEntryPoint { + + override fun commence( + request: HttpServletRequest, + response: HttpServletResponse, + authException: AuthenticationException + ) { + handlerExceptionResolver.resolveException(request, response, null, authException) + } +} diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandlerTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandlerTest.kt new file mode 100644 index 00000000..6151ceaf --- /dev/null +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandlerTest.kt @@ -0,0 +1,50 @@ +package com.ttasjwi.board.system.external.spring.security.exception + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.security.access.AccessDeniedException +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.web.servlet.HandlerExceptionResolver +import org.springframework.web.servlet.ModelAndView + +@DisplayName("CustomAccessDeniedHandler 테스트") +class CustomAccessDeniedHandlerTest { + + private lateinit var accessDeniedHandler: AccessDeniedHandler + private lateinit var handlerExceptionResolver: HandlerExceptionResolver + + @BeforeEach + fun setup() { + handlerExceptionResolver = mockk() + accessDeniedHandler = CustomAccessDeniedHandler(handlerExceptionResolver) + } + + + @Test + @DisplayName("내부적으로 HandlerExceptionResolver 를 호출한다.") + fun handleAccessDeniedException() { + // given + val request = mockk() + val response = mockk() + val exception = AccessDeniedException("권한 부족") + + every { handlerExceptionResolver.resolveException( + request, + response, + null, + exception + ) } returns ModelAndView() + + // when + accessDeniedHandler.handle(request, response, exception) + + // then + verify(exactly = 1) { handlerExceptionResolver.resolveException(request, response, null, exception) } + } +} diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPointTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPointTest.kt new file mode 100644 index 00000000..940eabdd --- /dev/null +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPointTest.kt @@ -0,0 +1,52 @@ +package com.ttasjwi.board.system.external.spring.security.exception + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.springframework.security.authentication.InsufficientAuthenticationException +import org.springframework.security.web.AuthenticationEntryPoint +import org.springframework.web.servlet.HandlerExceptionResolver +import org.springframework.web.servlet.ModelAndView + +@DisplayName("CustomAuthenticationEntryPoint 테스트") +class CustomAuthenticationEntryPointTest { + + private lateinit var authenticationEntryPoint: AuthenticationEntryPoint + private lateinit var handlerExceptionResolver: HandlerExceptionResolver + + @BeforeEach + fun setup() { + handlerExceptionResolver = mockk() + authenticationEntryPoint = CustomAuthenticationEntryPoint(handlerExceptionResolver) + } + + + @Test + @DisplayName("내부적으로 HandlerExceptionResolver 를 호출한다.") + fun handleAccessDeniedException() { + // given + val request = mockk() + val response = mockk() + val exception = InsufficientAuthenticationException("Full authentication is required to access this resource") + + every { + handlerExceptionResolver.resolveException( + request, + response, + null, + exception + ) + } returns ModelAndView() + + // when + authenticationEntryPoint.commence(request, response, exception) + + // then + verify(exactly = 1) { handlerExceptionResolver.resolveException(request, response, null, exception) } + } +} From 230ad5231ebddc56e2bc5730a148171a179ed134 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 10:57:00 +0900 Subject: [PATCH 10/14] =?UTF-8?q?Feature:=20(BRD-74)=20NoResourceFoundExce?= =?UTF-8?q?ption=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../api/GlobalExceptionController.kt | 13 ++++++++++ .../api/GlobalExceptionControllerTest.kt | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/board-system-external/external-exception-handle/src/main/kotlin/com/ttasjwi/board/system/core/exception/api/GlobalExceptionController.kt b/board-system-external/external-exception-handle/src/main/kotlin/com/ttasjwi/board/system/core/exception/api/GlobalExceptionController.kt index 05182bd8..180770a1 100644 --- a/board-system-external/external-exception-handle/src/main/kotlin/com/ttasjwi/board/system/core/exception/api/GlobalExceptionController.kt +++ b/board-system-external/external-exception-handle/src/main/kotlin/com/ttasjwi/board/system/core/exception/api/GlobalExceptionController.kt @@ -10,6 +10,7 @@ import com.ttasjwi.board.system.logging.getLogger import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.ExceptionHandler import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.servlet.resource.NoResourceFoundException @RestControllerAdvice internal class GlobalExceptionController( @@ -62,6 +63,18 @@ internal class GlobalExceptionController( ) } + @ExceptionHandler(NoResourceFoundException::class) + fun handleNoResourceFoundException(e: NoResourceFoundException): ResponseEntity { + return makeSingleErrorResponse( + errorStatus = ErrorStatus.NOT_FOUND, + errorItem = makeErrorItem( + code = "Error.ResourceNotFound", + args = listOf(e.httpMethod.name(), "/${e.resourcePath}"), + source = "httpMethod,resourcePath", + ) + ) + } + private fun handleNotImplementedError(e: NotImplementedError): ResponseEntity { log.error(e) diff --git a/board-system-external/external-exception-handle/src/test/kotlin/com/ttasjwi/board/system/core/exception/api/GlobalExceptionControllerTest.kt b/board-system-external/external-exception-handle/src/test/kotlin/com/ttasjwi/board/system/core/exception/api/GlobalExceptionControllerTest.kt index a6e05d0b..4be2e990 100644 --- a/board-system-external/external-exception-handle/src/test/kotlin/com/ttasjwi/board/system/core/exception/api/GlobalExceptionControllerTest.kt +++ b/board-system-external/external-exception-handle/src/test/kotlin/com/ttasjwi/board/system/core/exception/api/GlobalExceptionControllerTest.kt @@ -10,7 +10,9 @@ import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.springframework.http.HttpMethod import org.springframework.http.HttpStatus +import org.springframework.web.servlet.resource.NoResourceFoundException import java.util.* @@ -100,6 +102,28 @@ class GlobalExceptionControllerTest { assertThat(errorItem.source).isEqualTo("server") } + @Test + @DisplayName("NoResourceFoundException 처리 테스트") + fun handleNoResourceFound() { + val e = NoResourceFoundException(HttpMethod.GET, "fire/punch") + + val responseEntity = exceptionController.handleNoResourceFoundException(e) + val response = responseEntity.body as ErrorResponse + + assertThat(responseEntity.statusCode.value()).isEqualTo(HttpStatus.NOT_FOUND.value()) + assertThat(response.isSuccess).isFalse() + assertThat(response.code).isEqualTo("Error.Occurred") + assertThat(response.message).isEqualTo("Error.Occurred.message(locale=${Locale.KOREAN},args=[])") + assertThat(response.description).isEqualTo("Error.Occurred.description(locale=${Locale.KOREAN},args=[])") + assertThat(response.errors.size).isEqualTo(1) + + val errorItem = response.errors[0] + assertThat(errorItem.code).isEqualTo("Error.ResourceNotFound") + assertThat(errorItem.message).isEqualTo("Error.ResourceNotFound.message(locale=${Locale.KOREAN},args=[])") + assertThat(errorItem.description).isEqualTo("Error.ResourceNotFound.description(locale=${Locale.KOREAN},args=[${e.httpMethod.name()}, /${e.resourcePath}])") + assertThat(errorItem.source).isEqualTo("httpMethod,resourcePath") + } + @Test @DisplayName("ValidationExceptionCollector 을 잘 handle 하는 지 테스트") fun handleValidationExceptionCollectorTest() { @@ -132,4 +156,5 @@ class GlobalExceptionControllerTest { assertThat(errorItem2.description).isEqualTo("${exception2.code}.description(locale=${Locale.KOREAN},args=${exception2.args})") assertThat(errorItem2.source).isEqualTo(exception2.source) } + } From edcd3ab5cd3e79e73d985708aff414621a9ae00d Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 11:48:05 +0900 Subject: [PATCH 11/14] =?UTF-8?q?Feature:=20(BRD-74)=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=ED=95=84=EC=9A=94/=EC=9D=B8=EA=B0=80=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/exception/AccessDeniedException.kt | 15 +++++++ .../exception/UnauthenticatedException.kt | 15 +++++++ .../exception/AccessDeniedExceptionTest.kt | 40 +++++++++++++++++++ .../exception/UnauthenticatedExceptionTest.kt | 40 +++++++++++++++++++ 4 files changed, 110 insertions(+) create mode 100644 board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessDeniedException.kt create mode 100644 board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/UnauthenticatedException.kt create mode 100644 board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessDeniedExceptionTest.kt create mode 100644 board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/UnauthenticatedExceptionTest.kt diff --git a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessDeniedException.kt b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessDeniedException.kt new file mode 100644 index 00000000..67871d08 --- /dev/null +++ b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessDeniedException.kt @@ -0,0 +1,15 @@ +package com.ttasjwi.board.system.auth.domain.exception + +import com.ttasjwi.board.system.core.exception.CustomException +import com.ttasjwi.board.system.core.exception.ErrorStatus + +class AccessDeniedException( + cause: Throwable? = null, +) : CustomException( + status = ErrorStatus.FORBIDDEN, + code = "Error.AccessDenied", + args = emptyList(), + source = "credentials", + debugMessage = "리소스에 접근할 수 없습니다. 해당 리소스에 접근할 권한이 없습니다.", + cause = cause +) diff --git a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/UnauthenticatedException.kt b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/UnauthenticatedException.kt new file mode 100644 index 00000000..02add523 --- /dev/null +++ b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/exception/UnauthenticatedException.kt @@ -0,0 +1,15 @@ +package com.ttasjwi.board.system.auth.domain.exception + +import com.ttasjwi.board.system.core.exception.CustomException +import com.ttasjwi.board.system.core.exception.ErrorStatus + +class UnauthenticatedException( + cause: Throwable? = null, +) : CustomException( + status = ErrorStatus.UNAUTHENTICATED, + code = "Error.Unauthenticated", + args = emptyList(), + source = "credentials", + debugMessage = "리소스에 접근할 수 없습니다. 이 리소스에 접근하기 위해서는 인증이 필요합니다.", + cause = cause +) diff --git a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessDeniedExceptionTest.kt b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessDeniedExceptionTest.kt new file mode 100644 index 00000000..4c296502 --- /dev/null +++ b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/AccessDeniedExceptionTest.kt @@ -0,0 +1,40 @@ +package com.ttasjwi.board.system.auth.domain.exception + +import com.ttasjwi.board.system.core.exception.ErrorStatus +import com.ttasjwi.board.system.core.exception.fixture.customExceptionFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("AccessDeniedException: 인가 실패, 권한부족 상황에서 발생하는 예외") +class AccessDeniedExceptionTest { + + @Test + @DisplayName("예외 기본값 테스트") + fun test1() { + val exception = AccessDeniedException() + + assertThat(exception.status).isEqualTo(ErrorStatus.FORBIDDEN) + assertThat(exception.code).isEqualTo("Error.AccessDenied") + assertThat(exception.source).isEqualTo("credentials") + assertThat(exception.args).isEmpty() + assertThat(exception.message).isEqualTo(exception.debugMessage) + assertThat(exception.cause).isNull() + assertThat(exception.debugMessage).isEqualTo("리소스에 접근할 수 없습니다. 해당 리소스에 접근할 권한이 없습니다.") + } + + @Test + @DisplayName("근원 예외를 전달하여 생성할 수 있다.") + fun test2() { + val cause = customExceptionFixture() + val exception = AccessDeniedException(cause) + + assertThat(exception.status).isEqualTo(ErrorStatus.FORBIDDEN) + assertThat(exception.code).isEqualTo("Error.AccessDenied") + assertThat(exception.source).isEqualTo("credentials") + assertThat(exception.args).isEmpty() + assertThat(exception.message).isEqualTo(exception.debugMessage) + assertThat(exception.cause).isEqualTo(cause) + assertThat(exception.debugMessage).isEqualTo("리소스에 접근할 수 없습니다. 해당 리소스에 접근할 권한이 없습니다.") + } +} diff --git a/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/UnauthenticatedExceptionTest.kt b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/UnauthenticatedExceptionTest.kt new file mode 100644 index 00000000..97da886a --- /dev/null +++ b/board-system-domain/domain-auth/src/test/kotlin/com/ttasjwi/board/system/auth/domain/exception/UnauthenticatedExceptionTest.kt @@ -0,0 +1,40 @@ +package com.ttasjwi.board.system.auth.domain.exception + +import com.ttasjwi.board.system.core.exception.ErrorStatus +import com.ttasjwi.board.system.core.exception.fixture.customExceptionFixture +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +@DisplayName("UnauthenticatedException: 인증이 필요할 때 발생하는 예외") +class UnauthenticatedExceptionTest { + + @Test + @DisplayName("예외 기본값 테스트") + fun test1() { + val exception = UnauthenticatedException() + + assertThat(exception.status).isEqualTo(ErrorStatus.UNAUTHENTICATED) + assertThat(exception.code).isEqualTo("Error.Unauthenticated") + assertThat(exception.source).isEqualTo("credentials") + assertThat(exception.args).isEmpty() + assertThat(exception.message).isEqualTo(exception.debugMessage) + assertThat(exception.cause).isNull() + assertThat(exception.debugMessage).isEqualTo("리소스에 접근할 수 없습니다. 이 리소스에 접근하기 위해서는 인증이 필요합니다.") + } + + @Test + @DisplayName("근원 예외를 전달하여 생성할 수 있다.") + fun test2() { + val cause = customExceptionFixture() + val exception = UnauthenticatedException(cause) + + assertThat(exception.status).isEqualTo(ErrorStatus.UNAUTHENTICATED) + assertThat(exception.code).isEqualTo("Error.Unauthenticated") + assertThat(exception.source).isEqualTo("credentials") + assertThat(exception.args).isEmpty() + assertThat(exception.message).isEqualTo(exception.debugMessage) + assertThat(exception.cause).isEqualTo(cause) + assertThat(exception.debugMessage).isEqualTo("리소스에 접근할 수 없습니다. 이 리소스에 접근하기 위해서는 인증이 필요합니다.") + } +} From 435a828ebf93500f71cab9806260ea4ca1121ab6 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 12:01:50 +0900 Subject: [PATCH 12/14] =?UTF-8?q?Feature:=20(BRD-74)=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=ED=95=84=EC=9A=94/=EC=9D=B8=EA=B0=80=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 스프링 시큐리티에서 발생하는 인증필요/인가실패 예외를 커스텀예외로 변환하여 처리하도록 했다. --- .../exception/CustomAccessDeniedHandler.kt | 8 +++-- .../CustomAuthenticationEntryPoint.kt | 10 ++++++- .../CustomAccessDeniedHandlerTest.kt | 9 +++--- .../CustomAuthenticationEntryPointTest.kt | 30 +++++++++++++++++-- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandler.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandler.kt index eb9a6379..e7bf7fdd 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandler.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandler.kt @@ -1,8 +1,8 @@ package com.ttasjwi.board.system.external.spring.security.exception +import com.ttasjwi.board.system.auth.domain.exception.AccessDeniedException import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse -import org.springframework.security.access.AccessDeniedException import org.springframework.security.web.access.AccessDeniedHandler import org.springframework.web.servlet.HandlerExceptionResolver @@ -13,8 +13,10 @@ class CustomAccessDeniedHandler( override fun handle( request: HttpServletRequest, response: HttpServletResponse, - accessDeniedException: AccessDeniedException + accessDeniedException: org.springframework.security.access.AccessDeniedException ) { - handlerExceptionResolver.resolveException(request, response, null, accessDeniedException) + val customAccessDeniedException = AccessDeniedException(accessDeniedException) + + handlerExceptionResolver.resolveException(request, response, null, customAccessDeniedException) } } diff --git a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPoint.kt b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPoint.kt index d6ef27e0..06e50f6b 100644 --- a/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPoint.kt +++ b/board-system-external/external-security/src/main/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPoint.kt @@ -1,7 +1,9 @@ package com.ttasjwi.board.system.external.spring.security.exception +import com.ttasjwi.board.system.auth.domain.exception.UnauthenticatedException import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse +import org.springframework.security.authentication.InsufficientAuthenticationException import org.springframework.security.core.AuthenticationException import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.web.servlet.HandlerExceptionResolver @@ -15,6 +17,12 @@ class CustomAuthenticationEntryPoint( response: HttpServletResponse, authException: AuthenticationException ) { - handlerExceptionResolver.resolveException(request, response, null, authException) + var ex: Exception = authException + + if (authException is InsufficientAuthenticationException) { + ex = UnauthenticatedException(authException) + } + + handlerExceptionResolver.resolveException(request, response, null, ex) } } diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandlerTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandlerTest.kt index 6151ceaf..1677ef16 100644 --- a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandlerTest.kt +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAccessDeniedHandlerTest.kt @@ -1,5 +1,6 @@ package com.ttasjwi.board.system.external.spring.security.exception +import com.ttasjwi.board.system.auth.domain.exception.AccessDeniedException import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -8,7 +9,6 @@ import jakarta.servlet.http.HttpServletResponse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test -import org.springframework.security.access.AccessDeniedException import org.springframework.security.web.access.AccessDeniedHandler import org.springframework.web.servlet.HandlerExceptionResolver import org.springframework.web.servlet.ModelAndView @@ -25,26 +25,25 @@ class CustomAccessDeniedHandlerTest { accessDeniedHandler = CustomAccessDeniedHandler(handlerExceptionResolver) } - @Test @DisplayName("내부적으로 HandlerExceptionResolver 를 호출한다.") fun handleAccessDeniedException() { // given val request = mockk() val response = mockk() - val exception = AccessDeniedException("권한 부족") + val exception = org.springframework.security.access.AccessDeniedException("권한 부족") every { handlerExceptionResolver.resolveException( request, response, null, - exception + any(AccessDeniedException::class) ) } returns ModelAndView() // when accessDeniedHandler.handle(request, response, exception) // then - verify(exactly = 1) { handlerExceptionResolver.resolveException(request, response, null, exception) } + verify(exactly = 1) { handlerExceptionResolver.resolveException(request, response, null, any(AccessDeniedException::class)) } } } diff --git a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPointTest.kt b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPointTest.kt index 940eabdd..c2d49f47 100644 --- a/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPointTest.kt +++ b/board-system-external/external-security/src/test/kotlin/com/ttasjwi/board/system/external/spring/security/exception/CustomAuthenticationEntryPointTest.kt @@ -1,5 +1,6 @@ package com.ttasjwi.board.system.external.spring.security.exception +import com.ttasjwi.board.system.auth.domain.exception.UnauthenticatedException import io.mockk.every import io.mockk.mockk import io.mockk.verify @@ -8,6 +9,7 @@ import jakarta.servlet.http.HttpServletResponse import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.springframework.security.authentication.BadCredentialsException import org.springframework.security.authentication.InsufficientAuthenticationException import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.web.servlet.HandlerExceptionResolver @@ -25,14 +27,38 @@ class CustomAuthenticationEntryPointTest { authenticationEntryPoint = CustomAuthenticationEntryPoint(handlerExceptionResolver) } + @Test + @DisplayName("InsufficientAuthenticationException 은 인증이 필요하다는 예외이고, UnauthenticatedException 으로 변환해서 처리한다.") + fun handleInsufficientAuthenticationException() { + // given + val request = mockk() + val response = mockk() + val exception = InsufficientAuthenticationException("Full authentication is required to access this resource") + + every { + handlerExceptionResolver.resolveException( + request, + response, + null, + any(UnauthenticatedException::class) + ) + } returns ModelAndView() + + // when + authenticationEntryPoint.commence(request, response, exception) + + // then + verify(exactly = 1) { handlerExceptionResolver.resolveException(request, response, null, any(UnauthenticatedException::class)) } + } + @Test @DisplayName("내부적으로 HandlerExceptionResolver 를 호출한다.") - fun handleAccessDeniedException() { + fun handleAuthenticationException() { // given val request = mockk() val response = mockk() - val exception = InsufficientAuthenticationException("Full authentication is required to access this resource") + val exception = BadCredentialsException("password") every { handlerExceptionResolver.resolveException( From 1602a6544d0c78247b8a51a834194a205da2b9b2 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 12:53:46 +0900 Subject: [PATCH 13/14] =?UTF-8?q?Feature:=20(BRD-74)=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80/=EA=B5=AD=EC=A0=9C=ED=99=94=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/resources/message/error-message_en.yml | 15 +++++++++++++++ .../main/resources/message/error-message_ko.yml | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/board-system-external/external-message/src/main/resources/message/error-message_en.yml b/board-system-external/external-message/src/main/resources/message/error-message_en.yml index f5b45786..970c6063 100644 --- a/board-system-external/external-message/src/main/resources/message/error-message_en.yml +++ b/board-system-external/external-message/src/main/resources/message/error-message_en.yml @@ -8,6 +8,9 @@ Error: NotImplemented: message: "Feature Not Implemented" description: "The requested feature is currently not implemented. It is under development and is expected to be added in the near future." + ResourceNotFound: + message: "Resource Not Found" + description: "The requested resource could not be found. (httpMethod={0}, resourcePath=''{1}'')" NullArgument: message: "Missing required value" @@ -51,9 +54,21 @@ Error: LoginFailure: message: "Login failed" description: "Login failed. The email or password is incorrect." + InvalidAuthorizationHeaderFormat: + message: "Invalid Authorization Header" + description: "The format of the Authorization header is incorrect. Please send the token in the format ''Bearer [token]'' in the Authorization header." InvalidAccessTokenFormat: message: "Invalid access token format" description: "The access token format is invalid. The token value is incorrect or it is not an access token." InvalidRefreshTokenFormat: message: "Invalid refresh token format" description: "The refresh token format is invalid. The token value is incorrect or it is not a refresh token." + AccessTokenExpired: + message: "Access Token Expired" + description: "The access token has expired and is no longer valid. (Expiration Time={0}, Current Time={1}) Please refresh using a refresh token. If the refresh token has also expired, you will need to log in again." + Unauthenticated: + message: "Unauthenticated" + description: "Access to this resource is denied. Authentication is required to access this resource." + AccessDenied: + message: "Access Denied" + description: "You do not have permission to access this resource." diff --git a/board-system-external/external-message/src/main/resources/message/error-message_ko.yml b/board-system-external/external-message/src/main/resources/message/error-message_ko.yml index 9ca13421..e237fa61 100644 --- a/board-system-external/external-message/src/main/resources/message/error-message_ko.yml +++ b/board-system-external/external-message/src/main/resources/message/error-message_ko.yml @@ -8,6 +8,9 @@ Error: NotImplemented: message: "미구현 기능" description: "요청하신 기능은 현재 미구현 상태입니다. 해당 기능은 개발 중이며, 가까운 시일 내에 추가될 예정입니다." + ResourceNotFound: + message: "리소스를 찾을 수 없음" + description: "요청한 리소스를 찾을 수 없습니다. (httpMethod={0},resourcePath=''{1}'')" NullArgument: message: "필수값 누락" @@ -51,9 +54,21 @@ Error: LoginFailure: message: "로그인 실패" description: "로그인에 실패했습니다. 이메일 또는 패스워드가 잘못됐습니다." + InvalidAuthorizationHeaderFormat: + message: "Authorization 헤더값 오류" + description: "잘못된 Authorization 헤더 형식입니다. 토큰값을 Authorization 헤더에 ''Bearer [토큰값]'' 형식으로 보내주세요." InvalidAccessTokenFormat: message: "유효하지 않은 액세스토큰 포맷" description: "액세스 토큰 포맷이 유효하지 않습니다. 토큰값이 잘못됐거나, 액세스 토큰이 아닙니다." InvalidRefreshTokenFormat: message: "유효하지 않은 리프레시토큰 포맷" description: "리프레시 토큰 포맷이 유효하지 않습니다. 토큰값이 잘못됐거나, 리프레시 토큰이 아닙니다." + AccessTokenExpired: + message: "액세스토큰 만료됨" + description: "액세스 토큰이 만료되어 더 이상 유효하지 않습니다.(만료시각={0},현재시각={1}) 리프레시 토큰을 통해 갱신해주세요. 리프레시 토큰도 만료됐다면 로그인을 다시 하셔야합니다." + Unauthenticated: + message: "인증되지 않음" + description: "리소스에 접근할 수 없습니다. 이 리소스에 접근하기 위해서는 인증이 필요합니다." + AccessDenied: + message: "리소스 접근 권한 없음" + description: "리소스에 접근할 수 없습니다. 해당 리소스에 접근할 권한이 없습니다." From 324ba8a59f0f3beb7dd5709d81496669e619ec40 Mon Sep 17 00:00:00 2001 From: ttasjwi Date: Sun, 17 Nov 2024 12:57:35 +0900 Subject: [PATCH 14/14] =?UTF-8?q?Fix:=20(BRD-74)=20AccessToken.checkCurren?= =?UTF-8?q?tlyValid=20=EC=A0=91=EA=B7=BC=EC=A0=9C=EC=96=B4=EC=9E=90=20inte?= =?UTF-8?q?rnal=20=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ttasjwi/board/system/auth/domain/model/AccessToken.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/model/AccessToken.kt b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/model/AccessToken.kt index 69e611d1..bfd87a2e 100644 --- a/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/model/AccessToken.kt +++ b/board-system-domain/domain-auth/src/main/kotlin/com/ttasjwi/board/system/auth/domain/model/AccessToken.kt @@ -52,7 +52,7 @@ internal constructor( return result } - fun checkCurrentlyValid(currentTime: ZonedDateTime) { + internal fun checkCurrentlyValid(currentTime: ZonedDateTime) { if (currentTime >= this.expiresAt) { throw AccessTokenExpiredException(expiredAt = this.expiresAt, currentTime = currentTime) }