Skip to content

#13 - 카카오, 애플 소셜로그인 구현 #25

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions app/src/main/kotlin/com/wespot/ApiApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions app/src/main/kotlin/com/wespot/config/JpaAuditingConfig.kt
Original file line number Diff line number Diff line change
@@ -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 {
}
Copy link
Contributor

Choose a reason for hiding this comment

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

EOF 추가하면 좋을 것 같아요!!

32 changes: 32 additions & 0 deletions app/src/main/kotlin/com/wespot/config/OpenFeignConfig.kt
Original file line number Diff line number Diff line change
@@ -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<HttpMessageConverter<*>>): RestTemplate {
return RestTemplateBuilder().additionalMessageConverters(messageConverters).build()
}
Comment on lines +18 to +31
Copy link
Contributor

Choose a reason for hiding this comment

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

오호라, errorDecoder이외의 Converter, restTemplate은 꼭 필요한 설정인 것인가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이전에 로그상의 문제가 생겨서 작성했었는데,, 기억이 가물가물🥲
로그인쪽 다 되면 한 번 확인하고 필요 없다면 제거하겠습니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

근데 사실 뺄 필요는 없어요. 근거가 있는 것인지! 여쭤봤던거에요 호호호호

}
8 changes: 5 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@ allprojects {
}
}

repositories {
mavenCentral()
}

subprojects {
apply(plugin = "java")

apply(plugin = "kotlin")
apply(plugin = "kotlin-spring")
apply(plugin = "kotlin-kapt")
apply(plugin = "kotlin-noarg")
Copy link
Contributor

Choose a reason for hiding this comment

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

이놈짜슥.. 이거였구만

apply(plugin = "kotlin-jpa")

apply(plugin = "org.springframework.boot")
apply(plugin = "io.spring.dependency-management")
Expand All @@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

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

오호라 이거는 꼭 추가해야하는 의존성인가요?!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

저걸로 꼭 안해도 되긴하는데 없으면 힘드러요 흑흑

Copy link
Contributor

Choose a reason for hiding this comment

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

그럼 추가해야징~~~


testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
17 changes: 17 additions & 0 deletions common/src/main/kotlin/com/wespot/error/CustomErrorDecoder.kt
Original file line number Diff line number Diff line change
@@ -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")
Copy link
Contributor

Choose a reason for hiding this comment

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

Exception Message를 한글로 할 지, 영어로 할 지 현재는 통일성이 다소 떨어지는 것 같아요!(애플로그인 시 예외는 영어, 카카오 로그인 시 예외는 한글로 되어 있습니다.) 이 부분 함께 정해보면 좋을 것 같습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

한글로 하시죠!

}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

여기서 사용되는 ErrorDecoder는 OpenFeign에서만 사용되는것인가요?!

Copy link
Contributor

Choose a reason for hiding this comment

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

import 확인해보니까 feign errordecoder 맞군요

Copy link
Contributor Author

Choose a reason for hiding this comment

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

네 맞아요!

13 changes: 12 additions & 1 deletion core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,20 @@ 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")

// 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"))
implementation(project(":infrastructure:mysql"))
}

tasks.named<Jar>("jar") {
Expand Down
10 changes: 10 additions & 0 deletions core/src/main/kotlin/com/wespot/auth/dto/AuthLoginRequest.kt
Original file line number Diff line number Diff line change
@@ -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?
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.wespot.auth.dto

data class OAuthIdAndRefreshToken(
Copy link
Contributor

Choose a reason for hiding this comment

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

AuthLoginRequest와 동일하게 AuthLoginResponse도 괜찮을 것 같아요! 사용목적이 조금 더 잘 드러나지 않나 싶습니다.

Copy link
Contributor Author

@sectionr0 sectionr0 Jul 12, 2024

Choose a reason for hiding this comment

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

해당 dto가 외부(컨트롤러)로 나가지 않은 dto여서 response를 안 붙였는데,, 붙이는게 좋을까요??👀

Copy link
Contributor

Choose a reason for hiding this comment

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

아! 해당 DTO apple, kakao service가 반환하고 있어서 말씀드렸던 것인데, 컨트롤러로 나가지는 않는군요. 사실 And가 붙어서 그랬어요 흑흑 ㅠ

val oAuthId: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -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<ApplePublicKey>
) {
fun getMatchesKey(alg: String?, kid: String?): ApplePublicKey {
return keys
.firstOrNull { k -> k.alg == alg && k.kid == kid }
Copy link
Contributor

Choose a reason for hiding this comment

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

Kotlin은 String간의 비교도 == 연산으로 진행해도 되나보군요.
Kotlin 짱짱맨이네요 ;;

추가적으로 k가 아니라 key로 하시는 것은 어떤가요?!
축약어를 쓰는 것을 엄청 좋아하지는 않아서요.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

네 수정하겠습니다! 이런 사소한 캐치 너무 좋아요!

?: 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
)
Copy link
Contributor

Choose a reason for hiding this comment

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

물론 ApplePublicKeysResult에 포함되는 그저 data class이긴 하지만, 따로 class를 분리하지 않으신 이유가 있는 것일까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ApplePublicKey 가 ApplePublicKeysResult에서 사용되는 단순 데이터 구조였고, 긴밀하게 연관되어 있어서 같은 data class 끼리 묶었습니다 ㅎㅎ,,

분리해서 처리할게요!

Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
@@ -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?
)
20 changes: 20 additions & 0 deletions core/src/main/kotlin/com/wespot/auth/dto/apple/AppleTokenResult.kt
Original file line number Diff line number Diff line change
@@ -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?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.wespot.auth.dto.kakao

data class KakaoRevokeResult(
val id : Long,
)
Original file line number Diff line number Diff line change
@@ -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,
)
17 changes: 17 additions & 0 deletions core/src/main/kotlin/com/wespot/auth/dto/kakao/KakaoTokenResult.kt
Original file line number Diff line number Diff line change
@@ -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?,
)
Original file line number Diff line number Diff line change
@@ -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,
)
11 changes: 11 additions & 0 deletions core/src/main/kotlin/com/wespot/auth/service/SocialAuthService.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<SocialAuthService>
) {
fun getService(socialType: SocialType): SocialAuthService {
return services.find { it.isSupport(socialType) }
?: throw IllegalArgumentException("Unsupported social type: $socialType")
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

아주 멋있는 구조입니다~~
다만, AuthService라고 되어 있으니, Component말고 Service 어노테이션을 붙이는 것도 괜찮아보여요

Original file line number Diff line number Diff line change
@@ -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
}
}
46 changes: 46 additions & 0 deletions core/src/main/kotlin/com/wespot/auth/service/apple/AppleClient.kt
Original file line number Diff line number Diff line change
@@ -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])
Copy link
Contributor

Choose a reason for hiding this comment

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

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