From 87675db6420c476f060c5200c394fa2f9cf4505e Mon Sep 17 00:00:00 2001 From: sectionr0 Date: Fri, 12 Jul 2024 00:11:24 +0900 Subject: [PATCH 1/8] =?UTF-8?q?chore:=20kotlin=20noarg=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EB=B0=8F=20=EB=AA=A8=EB=93=88=20=EB=B0=A9=ED=96=A5?= =?UTF-8?q?=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gradle에 noarg, jpa 설정 - core, infrastruce 간의 모듈 방향 조정 --- build.gradle.kts | 8 +++++--- core/build.gradle.kts | 1 - infrastructure/mysql/build.gradle.kts | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 10b8d15e..f5655069 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -31,9 +31,6 @@ allprojects { } } -repositories { - mavenCentral() -} subprojects { apply(plugin = "java") @@ -41,6 +38,8 @@ subprojects { apply(plugin = "kotlin") apply(plugin = "kotlin-spring") apply(plugin = "kotlin-kapt") + apply(plugin = "kotlin-noarg") + apply(plugin = "kotlin-jpa") apply(plugin = "org.springframework.boot") apply(plugin = "io.spring.dependency-management") @@ -55,6 +54,9 @@ subprojects { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("mysql:mysql-connector-java:8.0.32") + // https://mvnrepository.com/artifact/org.bouncycastle/bcpkix-jdk15on + implementation("org.bouncycastle:bcpkix-jdk15on:1.69") + testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") testRuntimeOnly("org.junit.platform:junit-platform-launcher") diff --git a/core/build.gradle.kts b/core/build.gradle.kts index dbc2fd70..dfc52be5 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -4,7 +4,6 @@ dependencies { implementation(project(":common")) implementation(project(":domain")) - implementation(project(":infrastructure:mysql")) } tasks.named("jar") { diff --git a/infrastructure/mysql/build.gradle.kts b/infrastructure/mysql/build.gradle.kts index 3a7f3fc6..7cc3c986 100644 --- a/infrastructure/mysql/build.gradle.kts +++ b/infrastructure/mysql/build.gradle.kts @@ -1,12 +1,13 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar dependencies { - implementation("mysql:mysql-connector-java:8.0.32") + runtimeOnly("com.mysql:mysql-connector-j") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.data:spring-data-commons") implementation(project(":common")) implementation(project(":domain")) + implementation(project(":core")) } tasks.named("jar") { From c6d2784eae40a794f71dee70c0660e4c47ce7dee Mon Sep 17 00:00:00 2001 From: sectionr0 Date: Fri, 12 Jul 2024 00:12:25 +0900 Subject: [PATCH 2/8] =?UTF-8?q?refactor:=20entity=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - group이 예약어기 때문에, groupNumber로 변경 - VoteJpaEntity에 OneToMany 부분 수정 (Todo) - Social domain에 socialRefreshToken 추가 --- domain/src/main/kotlin/com/wespot/user/Social.kt | 1 + domain/src/main/kotlin/com/wespot/user/User.kt | 2 +- domain/src/main/kotlin/com/wespot/vote/Vote.kt | 2 +- .../kotlin/com/wespot/message/MessageJpaRepository.kt | 2 +- .../src/main/kotlin/com/wespot/user/SocialJpaEntity.kt | 1 - .../src/main/kotlin/com/wespot/user/UserJpaEntity.kt | 4 +--- .../src/main/kotlin/com/wespot/vote/VoteJpaEntity.kt | 8 ++++---- 7 files changed, 9 insertions(+), 11 deletions(-) diff --git a/domain/src/main/kotlin/com/wespot/user/Social.kt b/domain/src/main/kotlin/com/wespot/user/Social.kt index aa232c76..78994684 100644 --- a/domain/src/main/kotlin/com/wespot/user/Social.kt +++ b/domain/src/main/kotlin/com/wespot/user/Social.kt @@ -3,5 +3,6 @@ package com.wespot.user data class Social( val socialType: SocialType, val socialId: Long, + val socialRefreshToken: String ) { } \ No newline at end of file diff --git a/domain/src/main/kotlin/com/wespot/user/User.kt b/domain/src/main/kotlin/com/wespot/user/User.kt index d086518d..e486325f 100644 --- a/domain/src/main/kotlin/com/wespot/user/User.kt +++ b/domain/src/main/kotlin/com/wespot/user/User.kt @@ -7,7 +7,7 @@ data class User( val id: Long, val school: School, val grade: Int, - val group: Int, + val groupNumber: Int, val setting: Setting, val profile: Profile, val fcm: FCM, diff --git a/domain/src/main/kotlin/com/wespot/vote/Vote.kt b/domain/src/main/kotlin/com/wespot/vote/Vote.kt index 4ed7c7aa..314ad665 100644 --- a/domain/src/main/kotlin/com/wespot/vote/Vote.kt +++ b/domain/src/main/kotlin/com/wespot/vote/Vote.kt @@ -6,7 +6,7 @@ data class Vote( val id: Long, val schoolName: String, val grade: Int, - val group: Int, + val groupNumber: Int, val date: LocalDateTime, val ballots: List ) { diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/message/MessageJpaRepository.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/message/MessageJpaRepository.kt index fc99eaf8..d87f8fce 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/message/MessageJpaRepository.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/message/MessageJpaRepository.kt @@ -2,5 +2,5 @@ package com.wespot.message import org.springframework.data.jpa.repository.JpaRepository -interface MessageJpaRepository : JpaRepository { +interface MessageJpaRepository : JpaRepository { } \ No newline at end of file diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/SocialJpaEntity.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/SocialJpaEntity.kt index eb8267c3..2740e57c 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/user/SocialJpaEntity.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/SocialJpaEntity.kt @@ -12,7 +12,6 @@ class SocialJpaEntity( @field: NotNull val socialType: Long, - @field: NotNull val socialRefreshToken: String ) \ No newline at end of file diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/user/UserJpaEntity.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/user/UserJpaEntity.kt index b42aaa0c..212cb9d3 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/user/UserJpaEntity.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/user/UserJpaEntity.kt @@ -8,7 +8,6 @@ import java.time.LocalDateTime @Entity @Table(name = "user") class UserJpaEntity( - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long, @@ -20,7 +19,7 @@ class UserJpaEntity( val grade: Int, @field: NotNull - val group: Int, + val groupNumber: Int, @OneToOne(fetch = FetchType.LAZY) @JoinColumn( @@ -43,7 +42,6 @@ class UserJpaEntity( val fcm: FCMJpaEntity, @Embedded - @field: NotNull val social: SocialJpaEntity, @Embedded diff --git a/infrastructure/mysql/src/main/kotlin/com/wespot/vote/VoteJpaEntity.kt b/infrastructure/mysql/src/main/kotlin/com/wespot/vote/VoteJpaEntity.kt index abd9d872..97adc710 100644 --- a/infrastructure/mysql/src/main/kotlin/com/wespot/vote/VoteJpaEntity.kt +++ b/infrastructure/mysql/src/main/kotlin/com/wespot/vote/VoteJpaEntity.kt @@ -19,14 +19,14 @@ class VoteJpaEntity( val grade: Int, @field: NotNull - val group: Int, + val groupNumber: Int, @field: NotNull val date: LocalDateTime, - @field: NotNull - @OneToMany(mappedBy = "vote", cascade = [CascadeType.PERSIST]) - val ballots: List +// @field: NotNull +// @OneToMany(mappedBy = "vote", cascade = [CascadeType.PERSIST]) +// val ballots: List, ) From fba696564f0700054934df3a8efb7f7a9843e73a Mon Sep 17 00:00:00 2001 From: sectionr0 Date: Fri, 12 Jul 2024 00:13:30 +0900 Subject: [PATCH 3/8] =?UTF-8?q?refactor:=20config=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit JpaAuditingConfig, OpenFeignConfig 설정 --- .../main/kotlin/com/wespot/ApiApplication.kt | 2 -- .../com/wespot/config/JpaAuditingConfig.kt | 9 ++++++ .../com/wespot/config/OpenFeignConfig.kt | 32 +++++++++++++++++++ .../com/wespot/error/CustomErrorDecoder.kt | 17 ++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt create mode 100644 app/src/main/kotlin/com/wespot/config/OpenFeignConfig.kt create mode 100644 common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt diff --git a/app/src/main/kotlin/com/wespot/ApiApplication.kt b/app/src/main/kotlin/com/wespot/ApiApplication.kt index a6f41736..29dad63e 100644 --- a/app/src/main/kotlin/com/wespot/ApiApplication.kt +++ b/app/src/main/kotlin/com/wespot/ApiApplication.kt @@ -5,8 +5,6 @@ import org.springframework.boot.runApplication import org.springframework.cloud.openfeign.EnableFeignClients import org.springframework.data.jpa.repository.config.EnableJpaAuditing -@EnableJpaAuditing -@EnableFeignClients @SpringBootApplication(scanBasePackages = ["com.wespot"]) class ApiApplication diff --git a/app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt b/app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt new file mode 100644 index 00000000..ba48194e --- /dev/null +++ b/app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt @@ -0,0 +1,9 @@ +package com.wespot.config + +import org.springframework.context.annotation.Configuration +import org.springframework.data.jpa.repository.config.EnableJpaAuditing + +@Configuration +@EnableJpaAuditing +class JpaAuditingConfig { +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/wespot/config/OpenFeignConfig.kt b/app/src/main/kotlin/com/wespot/config/OpenFeignConfig.kt new file mode 100644 index 00000000..220bf59a --- /dev/null +++ b/app/src/main/kotlin/com/wespot/config/OpenFeignConfig.kt @@ -0,0 +1,32 @@ +package com.wespot.config + +import com.wespot.error.CustomErrorDecoder +import feign.codec.ErrorDecoder +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.cloud.openfeign.EnableFeignClients +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.converter.HttpMessageConverter +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter +import org.springframework.web.client.RestTemplate + + +@Configuration +@EnableFeignClients(basePackages = ["com.wespot"]) +class OpenFeignConfig { + + @Bean + fun errorDecoder(): ErrorDecoder { + return CustomErrorDecoder() + } + + @Bean + fun feignMessageConverter(): MappingJackson2HttpMessageConverter { + return MappingJackson2HttpMessageConverter() + } + + @Bean + fun restTemplate(messageConverters: List>): RestTemplate { + return RestTemplateBuilder().additionalMessageConverters(messageConverters).build() + } +} \ No newline at end of file diff --git a/common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt b/common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt new file mode 100644 index 00000000..18adb352 --- /dev/null +++ b/common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt @@ -0,0 +1,17 @@ +package com.wespot.error + +import feign.Response +import feign.codec.ErrorDecoder +import java.util.NoSuchElementException + +class CustomErrorDecoder : ErrorDecoder { + override fun decode(methodKey: String, response: Response): Exception { + return when (response.status()) { + 400 -> IllegalArgumentException("Bad request from OAuth") + 401 -> IllegalArgumentException("OAuth Authentication failed") + 404 -> NoSuchElementException("Resource not found from OAuth") + 500 -> RuntimeException("Internal server error from OAuth") + else -> RuntimeException("Unknown error occurred from OAuth") + } + } +} \ No newline at end of file From 8e4501a698636308aae8e80340fa956beee0ef3e Mon Sep 17 00:00:00 2001 From: sectionr0 Date: Fri, 12 Jul 2024 00:15:51 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=95=A0=ED=94=8C=20=EC=86=8C?= =?UTF-8?q?=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SocialAuthService 인터페이스, SocialAuthServiceFactory 컴포넌트 추가(전략 패턴 이용) --- core/build.gradle.kts | 5 ++ .../com/wespot/auth/dto/AuthLoginRequest.kt | 10 +++ .../wespot/auth/dto/OAuthIdAndRefreshToken.kt | 6 ++ .../auth/dto/apple/ApplePublicKeysResult.kt | 24 ++++++ .../auth/dto/apple/AppleRevokeRequest.kt | 16 ++++ .../auth/dto/apple/AppleTokenRequest.kt | 18 +++++ .../wespot/auth/dto/apple/AppleTokenResult.kt | 20 +++++ .../wespot/auth/service/SocialAuthService.kt | 11 +++ .../auth/service/SocialAuthServiceFactory.kt | 14 ++++ .../auth/service/SocialHeaderConfiguration.kt | 28 +++++++ .../wespot/auth/service/apple/AppleClient.kt | 46 +++++++++++ .../service/apple/AppleCreateClientSecret.kt | 57 +++++++++++++ .../auth/service/apple/AppleJwtParser.kt | 24 ++++++ .../service/apple/ApplePublicKeyGenerator.kt | 41 ++++++++++ .../wespot/auth/service/apple/AppleService.kt | 80 +++++++++++++++++++ 15 files changed, 400 insertions(+) create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/AuthLoginRequest.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/OAuthIdAndRefreshToken.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/apple/AppleRevokeRequest.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenRequest.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenResult.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/SocialAuthService.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/SocialAuthServiceFactory.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/SocialHeaderConfiguration.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/apple/AppleCreateClientSecret.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/apple/AppleJwtParser.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/apple/ApplePublicKeyGenerator.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/apple/AppleService.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index dfc52be5..e6ab0a9c 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -2,6 +2,11 @@ import org.springframework.boot.gradle.tasks.bundling.BootJar dependencies { + // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api + implementation("io.jsonwebtoken:jjwt-api:0.11.2") + implementation("io.jsonwebtoken:jjwt-impl:0.11.2") + implementation("io.jsonwebtoken:jjwt-jackson:0.11.2") + implementation(project(":common")) implementation(project(":domain")) } diff --git a/core/src/main/kotlin/com/wespot/auth/dto/AuthLoginRequest.kt b/core/src/main/kotlin/com/wespot/auth/dto/AuthLoginRequest.kt new file mode 100644 index 00000000..c011610c --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/AuthLoginRequest.kt @@ -0,0 +1,10 @@ +package com.wespot.auth.dto + +import com.wespot.user.SocialType + +data class AuthLoginRequest( + val socialType: SocialType, + val authorizationCode: String?, + val identityToken: String?, + val fcmToken: String? +) diff --git a/core/src/main/kotlin/com/wespot/auth/dto/OAuthIdAndRefreshToken.kt b/core/src/main/kotlin/com/wespot/auth/dto/OAuthIdAndRefreshToken.kt new file mode 100644 index 00000000..1f4fc25a --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/OAuthIdAndRefreshToken.kt @@ -0,0 +1,6 @@ +package com.wespot.auth.dto + +data class OAuthIdAndRefreshToken( + val oAuthId: String, + val refreshToken: String +) diff --git a/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt b/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt new file mode 100644 index 00000000..d0a2a8f6 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt @@ -0,0 +1,24 @@ +package com.wespot.auth.dto.apple + +/** + * https://developer.apple.com/documentation/sign_in_with_apple/fetch_apple_s_public_key_for_verifying_token_signature 참고 + */ +data class ApplePublicKeysResult( + // 공개키 목록 + val keys: List +) { + fun getMatchesKey(alg: String?, kid: String?): ApplePublicKey { + return keys + .firstOrNull { k -> k.alg == alg && k.kid == kid } + ?: throw IllegalArgumentException("Apple JWT 값의 alg, kid 정보가 올바르지 않습니다.") + } +} + +data class ApplePublicKey( + val kty: String, + val kid: String, + val use: String, + val alg: String, + val n: String, + val e: String +) \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleRevokeRequest.kt b/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleRevokeRequest.kt new file mode 100644 index 00000000..18ebbff2 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleRevokeRequest.kt @@ -0,0 +1,16 @@ +package com.wespot.auth.dto.apple + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens + */ +data class AppleRevokeRequest( + @JsonProperty("client_id") + val clientId: String, + @JsonProperty("client_secret") + val clientSecret: String, + val token: String, + @JsonProperty("token_type_hint") + val tokenTypeHint: String +) diff --git a/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenRequest.kt b/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenRequest.kt new file mode 100644 index 00000000..eb69911c --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenRequest.kt @@ -0,0 +1,18 @@ +package com.wespot.auth.dto.apple + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens 참고 + */ +data class AppleTokenRequest( + @JsonProperty("client_id") + val clientId: String, + @JsonProperty("client_secret") + val clientSecret: String, + val code: String?, + @JsonProperty("grant_type") + val grantType: String, + @JsonProperty("redirect_uri") + val redirectUri: String? +) \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenResult.kt b/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenResult.kt new file mode 100644 index 00000000..375fb0a0 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenResult.kt @@ -0,0 +1,20 @@ +package com.wespot.auth.dto.apple + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens 참고 + */ +data class AppleTokenResult( + @JsonProperty("access_token") + val accessToken: String, + @JsonProperty("token_type") + val tokenType: String, + @JsonProperty("expires_in") + val expiresIn: Int, + @JsonProperty("refresh_token") + val refreshToken: String?, + @JsonProperty("id_token") + val idToken: String, + val error: String?, +) diff --git a/core/src/main/kotlin/com/wespot/auth/service/SocialAuthService.kt b/core/src/main/kotlin/com/wespot/auth/service/SocialAuthService.kt new file mode 100644 index 00000000..8e2cc6ad --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/SocialAuthService.kt @@ -0,0 +1,11 @@ +package com.wespot.auth.service + +import com.wespot.auth.dto.AuthLoginRequest +import com.wespot.auth.dto.OAuthIdAndRefreshToken +import com.wespot.user.SocialType + +interface SocialAuthService { + fun fetchAuthToken(authLoginRequest: AuthLoginRequest): OAuthIdAndRefreshToken + fun isSupport(socialType: SocialType): Boolean + fun revoke(socialId: String, socialRefreshToken: String?): Boolean +} diff --git a/core/src/main/kotlin/com/wespot/auth/service/SocialAuthServiceFactory.kt b/core/src/main/kotlin/com/wespot/auth/service/SocialAuthServiceFactory.kt new file mode 100644 index 00000000..e7108235 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/SocialAuthServiceFactory.kt @@ -0,0 +1,14 @@ +package com.wespot.auth.service + +import com.wespot.user.SocialType +import org.springframework.stereotype.Component + +@Component +class SocialAuthServiceFactory( + private val services: List +) { + fun getService(socialType: SocialType): SocialAuthService { + return services.find { it.isSupport(socialType) } + ?: throw IllegalArgumentException("Unsupported social type: $socialType") + } +} diff --git a/core/src/main/kotlin/com/wespot/auth/service/SocialHeaderConfiguration.kt b/core/src/main/kotlin/com/wespot/auth/service/SocialHeaderConfiguration.kt new file mode 100644 index 00000000..b2c97f71 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/SocialHeaderConfiguration.kt @@ -0,0 +1,28 @@ +package com.wespot.auth.service + +import feign.Logger +import feign.RequestInterceptor +import feign.RequestTemplate +import org.springframework.context.annotation.Bean +import org.springframework.http.MediaType + +class SocialHeaderConfiguration { + companion object { + private const val CONTENT_TYPE_HEADER = "Content-Type" + } + + @Bean + fun requestInterceptor(): RequestInterceptor { + return RequestInterceptor { template: RequestTemplate -> + template.header( + CONTENT_TYPE_HEADER, + MediaType.APPLICATION_FORM_URLENCODED_VALUE + ) + } + } + + @Bean + fun feignLoggerLevel(): Logger.Level { + return Logger.Level.FULL + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt new file mode 100644 index 00000000..aa77d89e --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt @@ -0,0 +1,46 @@ +package com.wespot.auth.service.apple + +import com.wespot.auth.dto.apple.ApplePublicKeysResult +import com.wespot.auth.dto.apple.AppleRevokeRequest +import com.wespot.auth.dto.apple.AppleTokenResult +import com.wespot.auth.service.SocialHeaderConfiguration +import feign.Response +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestParam + +@FeignClient(name = "apple", url = "https://appleid.apple.com/auth", configuration = [SocialHeaderConfiguration::class]) +interface AppleClient { + + /** + * Apple 공개키 가져오기 + * https://appleid.apple.com/auth/keys + */ + @GetMapping("/keys") + fun getApplePublicKeys(): ApplePublicKeysResult + + /** + * Apple get Token + * https://developer.apple.com/documentation/sign_in_with_apple/generate_and_validate_tokens + */ + @PostMapping("/token", produces = [MediaType.APPLICATION_FORM_URLENCODED_VALUE]) + fun getToken( + @RequestParam("client_id") clientId: String, + @RequestParam("client_secret") clientSecret: String, + @RequestParam("code") code: String, + @RequestParam("grant_type") grantType: String, + @RequestParam("redirect_uri") redirectUri: String, + ): AppleTokenResult + + /** + * Apple revoke Token + * https://developer.apple.com/documentation/sign_in_with_apple/revoking_tokens + */ + @PostMapping("/revoke", produces = [MediaType.APPLICATION_FORM_URLENCODED_VALUE]) + fun revoke( + @RequestBody appleRevokeRequest: AppleRevokeRequest + ): Response +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleCreateClientSecret.kt b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleCreateClientSecret.kt new file mode 100644 index 00000000..61bc90a9 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleCreateClientSecret.kt @@ -0,0 +1,57 @@ +package com.wespot.auth.service.apple + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.SignatureAlgorithm +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component +import java.io.StringReader +import java.security.PrivateKey +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* + +@Component +class AppleCreateClientSecret( + @Value("\${apple.appleTeamId}") + private val appleTeamId: String, + + @Value("\${apple.appleAud}") + private val appleAud: String, + + @Value("\${apple.appleKeyId}") + private val appleKeyId: String, + + @Value("\${apple.appleKey}") + private val appleKey: String +) { + fun createClientSecret(): String { + val expirationDate = Date.from(LocalDateTime.now().plusDays(30).atZone(ZoneId.systemDefault()).toInstant()) + val jwtHeader: Map = mapOf("kid" to appleKeyId, "alg" to "ES256") + + return Jwts.builder() + .setHeader(jwtHeader) + .setIssuer(appleTeamId) + .setIssuedAt(Date(System.currentTimeMillis())) // 발행 시간 + .setExpiration(expirationDate) // 만료 시간 + .setAudience("https://appleid.apple.com") + .setSubject(appleAud) + .signWith(getPrivateKey(), SignatureAlgorithm.ES256) + .compact() + } + + private fun getPrivateKey(): PrivateKey { + return try { + val privateKeyContent = appleKey + val pemReader = StringReader(privateKeyContent) + val pemParser = PEMParser(pemReader) + val converter = JcaPEMKeyConverter() + val objects = pemParser.readObject() as PrivateKeyInfo + converter.getPrivateKey(objects) + } catch (e: Exception) { + throw IllegalArgumentException ("Failed to generate private key: ${e.message}") + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleJwtParser.kt b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleJwtParser.kt new file mode 100644 index 00000000..73524155 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleJwtParser.kt @@ -0,0 +1,24 @@ +package com.wespot.auth.service.apple + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.stereotype.Component +import java.util.* + +@Component +class AppleJwtParser(private val objectMapper: ObjectMapper) { + + companion object { + private const val HEADER_INDEX = 0 + } + + fun parseHeaders(identityToken: String): Map { + return try { + val encodedHeader = identityToken.split(".")[HEADER_INDEX] + val decodedHeader = String(Base64.getUrlDecoder().decode(encodedHeader)) + objectMapper.readValue(decodedHeader, object : TypeReference>() {}) + } catch (e: Exception) { + throw IllegalArgumentException("Apple OAuth Identity Token 형식이 올바르지 않습니다.") + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/service/apple/ApplePublicKeyGenerator.kt b/core/src/main/kotlin/com/wespot/auth/service/apple/ApplePublicKeyGenerator.kt new file mode 100644 index 00000000..fb1dbbd7 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/apple/ApplePublicKeyGenerator.kt @@ -0,0 +1,41 @@ +package com.wespot.auth.service.apple + +import com.wespot.auth.dto.apple.ApplePublicKeysResult +import org.springframework.stereotype.Component +import java.math.BigInteger +import java.security.KeyFactory +import java.security.PublicKey +import java.security.spec.RSAPublicKeySpec +import java.util.* + + +@Component +class ApplePublicKeyGenerator { + companion object { + private const val SIGN_ALGORITHM_HEADER_KEY = "alg" + private const val KEY_ID_HEADER_KEY = "kid" + private const val POSITIVE_SIGN_NUMBER = 1 + } + + fun generatePublicKey(headers: Map, applePublicKeys: ApplePublicKeysResult): PublicKey { + val applePublicKey = applePublicKeys.getMatchesKey( + headers[SIGN_ALGORITHM_HEADER_KEY], + headers[KEY_ID_HEADER_KEY] + ) + + val nBytes = Base64.getUrlDecoder().decode(applePublicKey.n) + val eBytes = Base64.getUrlDecoder().decode(applePublicKey.e) + + val n = BigInteger(POSITIVE_SIGN_NUMBER, nBytes) + val e = BigInteger(POSITIVE_SIGN_NUMBER, eBytes) + + val publicKeySpec = RSAPublicKeySpec(n, e) + + return try { + val keyFactory = KeyFactory.getInstance("RSA") + keyFactory.generatePublic(publicKeySpec) + } catch (exception: Exception) { + throw IllegalArgumentException("Apple OAuth 로그인 중 public key 생성에 문제가 발생했습니다.", exception) + } + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleService.kt b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleService.kt new file mode 100644 index 00000000..07fc8974 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleService.kt @@ -0,0 +1,80 @@ +package com.wespot.auth.service.apple + +import com.wespot.auth.dto.AuthLoginRequest +import com.wespot.auth.dto.OAuthIdAndRefreshToken +import com.wespot.auth.dto.apple.AppleRevokeRequest +import com.wespot.auth.dto.apple.AppleTokenResult +import com.wespot.auth.service.SocialAuthService +import com.wespot.user.SocialType +import io.jsonwebtoken.Claims +import io.jsonwebtoken.Jwts +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service + +@Service +class AppleService( + private val appleClient: AppleClient, + private val applePublicKeyGenerator: ApplePublicKeyGenerator, + private val appleJwtParser: AppleJwtParser, + private val appleCreateClientSecret: AppleCreateClientSecret, + @Value("\${apple.appleAud}") private val appleAud: String, + @Value("\${apple.appleRedirectUri}") private val appleRedirectUri: String, +) : SocialAuthService { + + companion object { + private const val NOT_SUPPORTED = "not supported" + private const val GRANT_TYPE = "authorization_code" + private const val TOKEN_TYPE_HINT = "refresh_token" + } + + override fun fetchAuthToken(authLoginRequest: AuthLoginRequest): OAuthIdAndRefreshToken { + val appleId = getAppleId(authLoginRequest.identityToken + ?: throw IllegalArgumentException("Apple ID token is null")) + val appleTokenResult = generateAuthToken(authLoginRequest.authorizationCode + ?: throw IllegalArgumentException("Authorization code is null")) + + return OAuthIdAndRefreshToken( + oAuthId = appleId, + refreshToken = appleTokenResult.refreshToken ?: NOT_SUPPORTED + ) + } + + override fun isSupport(socialType: SocialType): Boolean { + return socialType == SocialType.APPLE + } + + private fun getAppleId(identityToken: String): String { + val headers = appleJwtParser.parseHeaders(identityToken) + val applePublicKeys = appleClient.getApplePublicKeys() + val publicKey = applePublicKeyGenerator.generatePublicKey(headers, applePublicKeys) + val claims: Claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(identityToken).body + + return claims.subject + } + + private fun generateAuthToken(authorizationCode: String): AppleTokenResult { + val clientSecret = appleCreateClientSecret.createClientSecret() + + return appleClient.getToken( + clientId = appleAud, + clientSecret = clientSecret, + code = authorizationCode, + grantType = GRANT_TYPE, + redirectUri = appleRedirectUri + ) + } + + override fun revoke(socialId: String, socialRefreshToken: String?): Boolean { + val response = appleClient.revoke( + AppleRevokeRequest( + clientId = appleAud, + clientSecret = appleCreateClientSecret.createClientSecret(), + token = socialRefreshToken ?: throw IllegalArgumentException("Refresh token is null"), + tokenTypeHint = TOKEN_TYPE_HINT + ) + ) + + require(response.status() == 200) { "Failed to revoke the token. Status: ${response.status()}" } + return true + } +} From e1a528a51ac8be060f3cf8f52574f26ed0e6b0c1 Mon Sep 17 00:00:00 2001 From: sectionr0 Date: Fri, 12 Jul 2024 00:16:36 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카카오 소셜 로그인 추가 - SocialType에 None 추가 (어드민용) --- .../auth/dto/kakao/KakaoRevokeResult.kt | 5 ++ .../auth/dto/kakao/KakaoTokenRequest.kt | 16 +++++++ .../wespot/auth/dto/kakao/KakaoTokenResult.kt | 17 +++++++ .../auth/dto/kakao/KakaoUserInfoResult.kt | 9 ++++ .../wespot/auth/service/kakao/KakaoClient.kt | 36 ++++++++++++++ .../wespot/auth/service/kakao/KakaoService.kt | 48 +++++++++++++++++++ .../main/kotlin/com/wespot/user/SocialType.kt | 4 +- 7 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoRevokeResult.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenRequest.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenResult.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoUserInfoResult.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoClient.kt create mode 100644 core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoService.kt diff --git a/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoRevokeResult.kt b/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoRevokeResult.kt new file mode 100644 index 00000000..8fbf008d --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoRevokeResult.kt @@ -0,0 +1,5 @@ +package com.wespot.auth.dto.kakao + +data class KakaoRevokeResult( + val id : Long, +) diff --git a/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenRequest.kt b/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenRequest.kt new file mode 100644 index 00000000..91658f80 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenRequest.kt @@ -0,0 +1,16 @@ +package com.wespot.auth.dto.kakao + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token-request-body 참고 + */ +data class KakaoTokenRequest( + @JsonProperty("grant_type") + val grantType: String, + @JsonProperty("client_id") + val clientId: String, + @JsonProperty("redirect_uri") + val redirectUri: String, + val code: String, +) diff --git a/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenResult.kt b/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenResult.kt new file mode 100644 index 00000000..45afe2c3 --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenResult.kt @@ -0,0 +1,17 @@ +package com.wespot.auth.dto.kakao + +import com.fasterxml.jackson.annotation.JsonProperty + +/** + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token-response 참고 + */ +data class KakaoTokenResult( + @JsonProperty("access_token") + val accessToken: String, + @JsonProperty("token_type") + val tokenType: String, + @JsonProperty("refresh_token") + val refreshToken: String?, + @JsonProperty("refresh_token_expires_in") + val refreshTokenExpiresIn: String?, +) diff --git a/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoUserInfoResult.kt b/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoUserInfoResult.kt new file mode 100644 index 00000000..ffbbdacd --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoUserInfoResult.kt @@ -0,0 +1,9 @@ +package com.wespot.auth.dto.kakao + +/** + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info-response 참고 + */ +data class KakaoUserInfoResult( + val id: Long, + val connected_at: String, +) diff --git a/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoClient.kt b/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoClient.kt new file mode 100644 index 00000000..628ac45b --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoClient.kt @@ -0,0 +1,36 @@ +package com.wespot.auth.service.kakao + +import com.wespot.auth.dto.kakao.KakaoRevokeResult +import com.wespot.auth.dto.kakao.KakaoUserInfoResult +import com.wespot.auth.service.SocialHeaderConfiguration +import org.springframework.cloud.openfeign.FeignClient +import org.springframework.http.MediaType +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestHeader +import org.springframework.web.bind.annotation.RequestParam + +@FeignClient(name = "kakao", url = "https://kapi.kakao.com", configuration = [SocialHeaderConfiguration::class]) +interface KakaoClient { + + /** + * 사용자 정보 가져오기 + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-user-info + */ + @GetMapping("/v2/user/me", produces = [MediaType.APPLICATION_JSON_VALUE]) + fun getUserInfo( + @RequestHeader("Authorization") authorization: String + ): KakaoUserInfoResult + + + /** + * 연결 끊기 + * https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#req-unlink + */ + @PostMapping("/v1/user/unlink", produces = [MediaType.APPLICATION_JSON_VALUE]) + fun unlink( + @RequestHeader("Authorization") adminKey: String, + @RequestParam("target_id_type") targetIdType: String, + @RequestParam("target_id") targetId: String + ): KakaoRevokeResult +} \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoService.kt b/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoService.kt new file mode 100644 index 00000000..ee84d89b --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoService.kt @@ -0,0 +1,48 @@ +package com.wespot.auth.service.kakao + +import com.wespot.auth.dto.AuthLoginRequest +import com.wespot.auth.dto.OAuthIdAndRefreshToken +import com.wespot.auth.service.SocialAuthService +import com.wespot.user.SocialType +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service + +@Service +class KakaoService( + private val kakaoClient: KakaoClient, + @Value("\${kakao.adminKey}") private val adminKey: String +) : SocialAuthService { + + companion object { + private const val NOT_SUPPORTED = "not supported" + private const val KAKAO_PREFIX = "KakaoAK " + } + + override fun fetchAuthToken(authLoginRequest: AuthLoginRequest): OAuthIdAndRefreshToken { + val kakaoId = getKakaoId(authLoginRequest.identityToken + ?: throw IllegalArgumentException("Kakao ID가 입력되지 않았습니다.")) + + return OAuthIdAndRefreshToken( + oAuthId = kakaoId, refreshToken = NOT_SUPPORTED + ) + } + + override fun isSupport(socialType: SocialType): Boolean { + return socialType == SocialType.KAKAO + } + + override fun revoke(socialId: String, socialRefreshToken: String?): Boolean { + kakaoClient.unlink( + adminKey = "$KAKAO_PREFIX$adminKey", + targetIdType = "user_id", + targetId = socialId + ) + return true + } + + private fun getKakaoId(accessToken: String): String { + val kakaoUserInfo = kakaoClient.getUserInfo("Bearer $accessToken") + require(kakaoUserInfo.id > 0) { "Kakao 로그인에 실패하였습니다. 사용자 정보를 가져오는 데 문제가 발생하였습니다." } + return kakaoUserInfo.id.toString() + } +} diff --git a/domain/src/main/kotlin/com/wespot/user/SocialType.kt b/domain/src/main/kotlin/com/wespot/user/SocialType.kt index 88019126..9d8b2a46 100644 --- a/domain/src/main/kotlin/com/wespot/user/SocialType.kt +++ b/domain/src/main/kotlin/com/wespot/user/SocialType.kt @@ -1,5 +1,7 @@ package com.wespot.user enum class SocialType { - APPLE, KAKAO + APPLE, + KAKAO, + NONE // Admin } \ No newline at end of file From 530ca34d4f9493f913621813626028ff2d421b3d Mon Sep 17 00:00:00 2001 From: sectionr0 Date: Fri, 12 Jul 2024 00:19:10 +0900 Subject: [PATCH 6/8] =?UTF-8?q?feat:=20kotest,=20SocialAuthServiceFactoryT?= =?UTF-8?q?est=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kotest+mockk gradle 추가 - SocialAuthServiceFactoryTest 추가 --- core/build.gradle.kts | 7 +++ .../service/SocialAuthServiceFactoryTest.kt | 48 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 core/src/test/kotlin/com/wespot/auth/service/SocialAuthServiceFactoryTest.kt diff --git a/core/build.gradle.kts b/core/build.gradle.kts index e6ab0a9c..cd6871c5 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -7,6 +7,13 @@ dependencies { implementation("io.jsonwebtoken:jjwt-impl:0.11.2") implementation("io.jsonwebtoken:jjwt-jackson:0.11.2") + // Test dependencies + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("io.kotest:kotest-runner-junit5:5.8.0") + testImplementation("io.kotest:kotest-assertions-core:5.8.0") + testImplementation("io.kotest:kotest-property:5.8.0") + testImplementation("io.mockk:mockk:1.13.7") + implementation(project(":common")) implementation(project(":domain")) } diff --git a/core/src/test/kotlin/com/wespot/auth/service/SocialAuthServiceFactoryTest.kt b/core/src/test/kotlin/com/wespot/auth/service/SocialAuthServiceFactoryTest.kt new file mode 100644 index 00000000..d588ef55 --- /dev/null +++ b/core/src/test/kotlin/com/wespot/auth/service/SocialAuthServiceFactoryTest.kt @@ -0,0 +1,48 @@ +package com.wespot.auth.service + +import com.wespot.auth.service.kakao.KakaoService +import com.wespot.auth.service.apple.AppleService +import com.wespot.user.SocialType +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.kotest.assertions.throwables.shouldThrow +import io.mockk.every +import io.mockk.mockk + +class SocialAuthServiceFactoryTest : BehaviorSpec({ + val kakaoService = mockk() + val appleService = mockk() + val factory = SocialAuthServiceFactory(listOf(kakaoService, appleService)) + + given("SocialAuthServiceFactory 테스트") { + every { kakaoService.isSupport(SocialType.KAKAO) } returns true + every { kakaoService.isSupport(SocialType.APPLE) } returns false + every { kakaoService.isSupport(SocialType.NONE) } returns false + + every { appleService.isSupport(SocialType.KAKAO) } returns false + every { appleService.isSupport(SocialType.APPLE) } returns true + every { appleService.isSupport(SocialType.NONE) } returns false + + `when`("Kakao 소셜 타입에 대한 서비스를 요청할 때") { + then("KakaoService를 반환해야 한다") { + val service = factory.getService(SocialType.KAKAO) + service shouldBe kakaoService + } + } + + `when`("Apple 소셜 타입에 대한 서비스를 요청할 때") { + then("AppleService를 반환해야 한다") { + val service = factory.getService(SocialType.APPLE) + service shouldBe appleService + } + } + + `when`("지원하지 않는 소셜 타입에 대한 서비스를 요청할 때") { + then("IllegalArgumentException을 던져야 한다") { + shouldThrow { + factory.getService(SocialType.NONE) + } + } + } + } +}) From c5a2357a529f6ff2049c7e8112fa463393ae3f50 Mon Sep 17 00:00:00 2001 From: sectionr0 Date: Sat, 13 Jul 2024 02:56:42 +0900 Subject: [PATCH 7/8] =?UTF-8?q?refactor:=20=EC=97=90=EB=9F=AC=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=B6=84=EB=A6=AC,=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EA=B0=80=EB=8F=85=EC=84=B1=20=EB=86=92=EC=9D=B4=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/wespot/config/JpaAuditingConfig.kt | 2 +- app/src/main/resources/backend-submodule | 2 +- .../kotlin/com/wespot/error/CustomErrorDecoder.kt | 12 ++++++------ .../com/wespot/auth/dto/apple/ApplePublicKey.kt | 10 ++++++++++ .../wespot/auth/dto/apple/ApplePublicKeysResult.kt | 9 --------- .../wespot/auth/service/SocialAuthServiceFactory.kt | 5 +++-- .../com/wespot/auth/service/apple/AppleClient.kt | 6 +++++- .../auth/service/apple/AppleCreateClientSecret.kt | 2 +- .../com/wespot/auth/service/apple/AppleService.kt | 10 +++++++--- .../com/wespot/auth/service/kakao/KakaoClient.kt | 6 +++++- 10 files changed, 39 insertions(+), 25 deletions(-) create mode 100644 core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKey.kt diff --git a/app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt b/app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt index ba48194e..927bf849 100644 --- a/app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt +++ b/app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt @@ -6,4 +6,4 @@ import org.springframework.data.jpa.repository.config.EnableJpaAuditing @Configuration @EnableJpaAuditing class JpaAuditingConfig { -} \ No newline at end of file +} diff --git a/app/src/main/resources/backend-submodule b/app/src/main/resources/backend-submodule index 7026cbec..0df1be66 160000 --- a/app/src/main/resources/backend-submodule +++ b/app/src/main/resources/backend-submodule @@ -1 +1 @@ -Subproject commit 7026cbec46e0ddbbd623b101eda33721c53744da +Subproject commit 0df1be660e1552143e8e5243c78736203b110bc0 diff --git a/common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt b/common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt index 18adb352..5125832c 100644 --- a/common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt +++ b/common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt @@ -7,11 +7,11 @@ import java.util.NoSuchElementException class CustomErrorDecoder : ErrorDecoder { override fun decode(methodKey: String, response: Response): Exception { return when (response.status()) { - 400 -> IllegalArgumentException("Bad request from OAuth") - 401 -> IllegalArgumentException("OAuth Authentication failed") - 404 -> NoSuchElementException("Resource not found from OAuth") - 500 -> RuntimeException("Internal server error from OAuth") - else -> RuntimeException("Unknown error occurred from OAuth") + 400 -> IllegalArgumentException("OAuth 요청이 잘못되었습니다 (Bad request)") + 401 -> IllegalArgumentException("OAuth 인증에 실패하였습니다 (Authentication failed)") + 404 -> NoSuchElementException("OAuth 리소스를 찾을 수 없습니다 (Resource not found)") + 500 -> RuntimeException("OAuth 내부 서버 오류입니다 (Internal server error)") + else -> RuntimeException("OAuth 연결 중 알 수 없는 오류가 발생했습니다)") } } -} \ No newline at end of file +} diff --git a/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKey.kt b/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKey.kt new file mode 100644 index 00000000..9c4bb81c --- /dev/null +++ b/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKey.kt @@ -0,0 +1,10 @@ +package com.wespot.auth.dto.apple + +data class ApplePublicKey( + val kty: String, + val kid: String, + val use: String, + val alg: String, + val n: String, + val e: String +) \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt b/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt index d0a2a8f6..c3b863ba 100644 --- a/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt +++ b/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt @@ -13,12 +13,3 @@ data class ApplePublicKeysResult( ?: throw IllegalArgumentException("Apple JWT 값의 alg, kid 정보가 올바르지 않습니다.") } } - -data class ApplePublicKey( - val kty: String, - val kid: String, - val use: String, - val alg: String, - val n: String, - val e: String -) \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/service/SocialAuthServiceFactory.kt b/core/src/main/kotlin/com/wespot/auth/service/SocialAuthServiceFactory.kt index e7108235..40547c07 100644 --- a/core/src/main/kotlin/com/wespot/auth/service/SocialAuthServiceFactory.kt +++ b/core/src/main/kotlin/com/wespot/auth/service/SocialAuthServiceFactory.kt @@ -2,13 +2,14 @@ package com.wespot.auth.service import com.wespot.user.SocialType import org.springframework.stereotype.Component +import org.springframework.stereotype.Service -@Component +@Service class SocialAuthServiceFactory( private val services: List ) { fun getService(socialType: SocialType): SocialAuthService { return services.find { it.isSupport(socialType) } - ?: throw IllegalArgumentException("Unsupported social type: $socialType") + ?: throw IllegalArgumentException("해당 소셜로그인을 지원하지 않습니다. social type: $socialType") } } diff --git a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt index aa77d89e..db838b97 100644 --- a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt +++ b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt @@ -12,7 +12,11 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestParam -@FeignClient(name = "apple", url = "https://appleid.apple.com/auth", configuration = [SocialHeaderConfiguration::class]) +@FeignClient( + name = "apple", + url = "https://appleid.apple.com/auth", + configuration = [SocialHeaderConfiguration::class] +) interface AppleClient { /** diff --git a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleCreateClientSecret.kt b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleCreateClientSecret.kt index 61bc90a9..24ddede0 100644 --- a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleCreateClientSecret.kt +++ b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleCreateClientSecret.kt @@ -51,7 +51,7 @@ class AppleCreateClientSecret( val objects = pemParser.readObject() as PrivateKeyInfo converter.getPrivateKey(objects) } catch (e: Exception) { - throw IllegalArgumentException ("Failed to generate private key: ${e.message}") + throw IllegalArgumentException ("Apple private key 생성 실패: ${e.message}") } } } \ No newline at end of file diff --git a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleService.kt b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleService.kt index 07fc8974..dabff5d1 100644 --- a/core/src/main/kotlin/com/wespot/auth/service/apple/AppleService.kt +++ b/core/src/main/kotlin/com/wespot/auth/service/apple/AppleService.kt @@ -29,9 +29,9 @@ class AppleService( override fun fetchAuthToken(authLoginRequest: AuthLoginRequest): OAuthIdAndRefreshToken { val appleId = getAppleId(authLoginRequest.identityToken - ?: throw IllegalArgumentException("Apple ID token is null")) + ?: throw IllegalArgumentException("Apple ID token이 없습니다.")) val appleTokenResult = generateAuthToken(authLoginRequest.authorizationCode - ?: throw IllegalArgumentException("Authorization code is null")) + ?: throw IllegalArgumentException("Authorization code가 없습니다.")) return OAuthIdAndRefreshToken( oAuthId = appleId, @@ -47,7 +47,11 @@ class AppleService( val headers = appleJwtParser.parseHeaders(identityToken) val applePublicKeys = appleClient.getApplePublicKeys() val publicKey = applePublicKeyGenerator.generatePublicKey(headers, applePublicKeys) - val claims: Claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(identityToken).body + val claims: Claims = Jwts.parserBuilder() + .setSigningKey(publicKey) + .build() + .parseClaimsJws(identityToken) + .body return claims.subject } diff --git a/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoClient.kt b/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoClient.kt index 628ac45b..4a44c3d3 100644 --- a/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoClient.kt +++ b/core/src/main/kotlin/com/wespot/auth/service/kakao/KakaoClient.kt @@ -10,7 +10,11 @@ import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestParam -@FeignClient(name = "kakao", url = "https://kapi.kakao.com", configuration = [SocialHeaderConfiguration::class]) +@FeignClient( + name = "kakao", + url = "https://kapi.kakao.com", + configuration = [SocialHeaderConfiguration::class] +) interface KakaoClient { /** From 067b593b8b4c0869a373e1d0b805a288129d0025 Mon Sep 17 00:00:00 2001 From: sectionr0 Date: Sat, 13 Jul 2024 03:01:33 +0900 Subject: [PATCH 8/8] =?UTF-8?q?refactor:=20socialType=20none=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wespot/auth/dto/apple/ApplePublicKeysResult.kt | 2 +- .../auth/service/SocialAuthServiceFactoryTest.kt | 10 ---------- domain/src/main/kotlin/com/wespot/user/SocialType.kt | 3 +-- 3 files changed, 2 insertions(+), 13 deletions(-) diff --git a/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt b/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt index c3b863ba..5ead7dce 100644 --- a/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt +++ b/core/src/main/kotlin/com/wespot/auth/dto/apple/ApplePublicKeysResult.kt @@ -9,7 +9,7 @@ data class ApplePublicKeysResult( ) { fun getMatchesKey(alg: String?, kid: String?): ApplePublicKey { return keys - .firstOrNull { k -> k.alg == alg && k.kid == kid } + .firstOrNull { key -> key.alg == alg && key.kid == kid } ?: throw IllegalArgumentException("Apple JWT 값의 alg, kid 정보가 올바르지 않습니다.") } } diff --git a/core/src/test/kotlin/com/wespot/auth/service/SocialAuthServiceFactoryTest.kt b/core/src/test/kotlin/com/wespot/auth/service/SocialAuthServiceFactoryTest.kt index d588ef55..e4870970 100644 --- a/core/src/test/kotlin/com/wespot/auth/service/SocialAuthServiceFactoryTest.kt +++ b/core/src/test/kotlin/com/wespot/auth/service/SocialAuthServiceFactoryTest.kt @@ -17,11 +17,9 @@ class SocialAuthServiceFactoryTest : BehaviorSpec({ given("SocialAuthServiceFactory 테스트") { every { kakaoService.isSupport(SocialType.KAKAO) } returns true every { kakaoService.isSupport(SocialType.APPLE) } returns false - every { kakaoService.isSupport(SocialType.NONE) } returns false every { appleService.isSupport(SocialType.KAKAO) } returns false every { appleService.isSupport(SocialType.APPLE) } returns true - every { appleService.isSupport(SocialType.NONE) } returns false `when`("Kakao 소셜 타입에 대한 서비스를 요청할 때") { then("KakaoService를 반환해야 한다") { @@ -36,13 +34,5 @@ class SocialAuthServiceFactoryTest : BehaviorSpec({ service shouldBe appleService } } - - `when`("지원하지 않는 소셜 타입에 대한 서비스를 요청할 때") { - then("IllegalArgumentException을 던져야 한다") { - shouldThrow { - factory.getService(SocialType.NONE) - } - } - } } }) diff --git a/domain/src/main/kotlin/com/wespot/user/SocialType.kt b/domain/src/main/kotlin/com/wespot/user/SocialType.kt index 9d8b2a46..07e7b574 100644 --- a/domain/src/main/kotlin/com/wespot/user/SocialType.kt +++ b/domain/src/main/kotlin/com/wespot/user/SocialType.kt @@ -2,6 +2,5 @@ package com.wespot.user enum class SocialType { APPLE, - KAKAO, - NONE // Admin + KAKAO } \ No newline at end of file