Skip to content

Commit

Permalink
feat: 카카오 인가 코드 API
Browse files Browse the repository at this point in the history
  • Loading branch information
DongGeon0908 committed Jul 29, 2024
1 parent c4af780 commit 3aeac01
Show file tree
Hide file tree
Showing 17 changed files with 267 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ jobs:
encrypt.algorithm: ${{ secrets.ENCRYPT_ALGORITHM }}
# dev resource 환경 변수 주입
hero-alignlab.dev.resource.key: ${{ secrets.HERO_ALIGNLAB_DEV_RESOURCE_KEY }}
# OAuth-Kakao 환경 변수 주입
oauth.kakao.rest-api-key: ${{ secrets.OAUTH_KAKAO_REST_API_KEY }}
oauth.kakao.client-secret-code: ${{ secrets.OAUTH_KAKAO_CLIENT_SECRET_CODE }}

# Secret Setup - application-prod.yml
- name: Inject env-values to application-prod.yml
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.hero.alignlab.client.kakao

import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.reactive.function.client.awaitBody
import org.springframework.web.reactive.function.client.awaitBodyOrNull

abstract class SuspendableClient(
protected val client: WebClient,
) {
protected suspend inline fun <reified T : Any> WebClient.RequestHeadersSpec<*>.request(): T {
return this.retrieve().awaitBody<T>()
}

protected suspend inline fun <reified T : Any> WebClient.RequestHeadersSpec<*>.requestOrNull(): T? {
return this.retrieve().awaitBodyOrNull<T>()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.hero.alignlab.client.kakao.client

interface KaKaoOAuthClient {
suspend fun getOAuthAuthorizeCode(redirectUrl: String? = null)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.hero.alignlab.client.kakao.client

import com.hero.alignlab.client.kakao.SuspendableClient
import com.hero.alignlab.client.kakao.config.KakaoOAuthClientConfig
import org.springframework.web.reactive.function.client.WebClient

class SuspendableKakaoOAuthClient(
client: WebClient,
private val config: KakaoOAuthClientConfig.Config,
) : KaKaoOAuthClient, SuspendableClient(client) {
override suspend fun getOAuthAuthorizeCode(redirectUrl: String?) {
client
.get()
.uri("/authorize") { builder ->
builder
.queryParam("response_type", "code")
.queryParam("client_id", config.restApiKey)
.queryParam("redirect_uri", redirectUrl ?: config.redirectUrl)
.build()
}.requestOrNull<Any>()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.hero.alignlab.client.kakao.config

import com.hero.alignlab.client.WebClientFactory
import com.hero.alignlab.client.kakao.client.KaKaoOAuthClient
import com.hero.alignlab.client.kakao.client.SuspendableKakaoOAuthClient
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.validation.Valid
import jakarta.validation.constraints.NotBlank
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.validation.annotation.Validated

@Validated
@Configuration
class KakaoOAuthClientConfig {
private val logger = KotlinLogging.logger { }

@Bean
@ConditionalOnProperty(prefix = "oauth.kakao", name = ["url"])
@ConfigurationProperties(prefix = "oauth.kakao")
fun kakaoOAuthConfig() = Config()

@Bean
@ConditionalOnBean(name = ["kakaoOAuthConfig"])
@ConditionalOnMissingBean(KaKaoOAuthClient::class)
fun kakaoOAuthClient(
@Valid kakaoOAuthConfig: Config
): KaKaoOAuthClient {
logger.info { "initialized kakaoOAuthClient. $kakaoOAuthConfig" }

val webclient = WebClientFactory.generate(kakaoOAuthConfig.url)

return SuspendableKakaoOAuthClient(webclient, kakaoOAuthConfig)
}

data class Config(
@field:NotBlank
var url: String = "",
@field:NotBlank
var restApiKey: String = "",
@field:NotBlank
var clientSecretCode: String = "",
@field:NotBlank
var redirectUrl: String = "",
@field:NotBlank
var devRedirectUrl: String = "",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.hero.alignlab.domain.auth.application

import com.hero.alignlab.client.kakao.client.KaKaoOAuthClient
import com.hero.alignlab.domain.auth.model.OAuthProvider
import com.hero.alignlab.domain.auth.model.response.OAuthAuthorizeCodeResponse
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service

@Service
class OAuthFacade(
private val kaKaoOAuthClient: KaKaoOAuthClient,
) {
private val logger = KotlinLogging.logger { }

suspend fun getOAuthAuthorizeCode(provider: OAuthProvider): OAuthAuthorizeCodeResponse {
when (provider) {
OAuthProvider.kakao -> kaKaoOAuthClient.getOAuthAuthorizeCode()
}

return OAuthAuthorizeCodeResponse(provider)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.hero.alignlab.domain.auth.model

enum class OAuthProvider {
kakao,
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.hero.alignlab.domain.auth.model.response

import com.hero.alignlab.domain.auth.model.OAuthProvider

data class OAuthAuthorizeCodeResponse(
val provider: OAuthProvider,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.hero.alignlab.domain.auth.resource

import com.hero.alignlab.common.extension.wrapOk
import com.hero.alignlab.domain.auth.application.OAuthFacade
import com.hero.alignlab.domain.auth.model.OAuthProvider
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@Tag(name = "OAuth 인증 및 인가 관리")
@RestController
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
class OAuthResource(
private val oAuthFacade: OAuthFacade,
) {
/**
* OAuth로 인가코드를 부여받고, 이를 Redirect Url로 반환.
* - redirectUrl은 Client의 주소값, 만약 변경시 yml 및 각 클라이언트 구조 변경 필요.
* - [Kakao Rest Auth](https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api)
*/
@Operation(summary = "인가 코드 받기")
@GetMapping("/api/v1/oauth/{provider}/authorize")
suspend fun getOAuthAuthorizeCode(
@RequestParam provider: OAuthProvider,
) = oAuthFacade.getOAuthAuthorizeCode(provider).wrapOk()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.hero.alignlab.domain.dev.application

import com.hero.alignlab.client.kakao.client.KaKaoOAuthClient
import com.hero.alignlab.client.kakao.config.KakaoOAuthClientConfig
import com.hero.alignlab.domain.auth.model.OAuthProvider
import com.hero.alignlab.domain.dev.model.response.DevOAuthCodeResponse
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service

@Service
class DevOAuthService(
private val kaKaoOAuthClient: KaKaoOAuthClient,
private val config: KakaoOAuthClientConfig.Config,
) {
private val logger = KotlinLogging.logger { }

suspend fun getOAuthAuthorizeCode(provider: OAuthProvider): DevOAuthCodeResponse {
when (provider) {
OAuthProvider.kakao -> kaKaoOAuthClient.getOAuthAuthorizeCode(config.devRedirectUrl)
}

return DevOAuthCodeResponse(provider)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.hero.alignlab.domain.dev.model.request

import com.fasterxml.jackson.databind.PropertyNamingStrategies
import com.fasterxml.jackson.databind.annotation.JsonNaming

@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy::class)
data class DevRedirectedRequest(
val code: String?,
val error: String?,
val errorDescription: String?,
val state: String?,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.hero.alignlab.domain.dev.model.response

import com.hero.alignlab.domain.auth.model.OAuthProvider

data class DevOAuthCodeResponse(
val provider: OAuthProvider,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.hero.alignlab.domain.dev.model.response

import com.hero.alignlab.domain.auth.model.OAuthProvider
import com.hero.alignlab.domain.dev.model.request.DevRedirectedRequest

data class DevRedirectedResponse(
val provider: OAuthProvider,
val requestParams: DevRedirectedRequest,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.hero.alignlab.domain.dev.resource

import com.hero.alignlab.common.extension.wrapOk
import com.hero.alignlab.domain.auth.model.OAuthProvider
import com.hero.alignlab.domain.dev.application.DevOAuthService
import com.hero.alignlab.domain.dev.model.request.DevRedirectedRequest
import com.hero.alignlab.domain.dev.model.response.DevRedirectedResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import org.springdoc.core.annotations.ParameterObject
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@Tag(name = "Dev OAuth 인증 및 인가 관리")
@RestController
@RequestMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
class DevOAuthResource(
private val devOAuthService: DevOAuthService,
) {
@Operation(summary = "인가 코드 받기")
@GetMapping("/api/dev/v1/oauth/{provider}/authorize")
suspend fun getDevOAuthAuthorizeCode(
@PathVariable provider: OAuthProvider,
) = devOAuthService.getOAuthAuthorizeCode(provider).wrapOk()

@Operation(summary = "OAuth Redirect Test")
@GetMapping("/api/dev/v1/oauth/{provider}/authorize/redirected")
suspend fun redirectedDevOAuthAuthorizeCode(
@PathVariable provider: OAuthProvider,
@ParameterObject request: DevRedirectedRequest,
) = DevRedirectedResponse(
provider = provider,
requestParams = request,
).wrapOk()
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class NotFoundException(errorCode: ErrorCode) : AlignlabException(errorCode)

class InvalidTokenException(errorCode: ErrorCode) : AlignlabException(errorCode)

class InvalidRequestException(errorCode: ErrorCode) : AlignlabException(errorCode)
class InvalidRequestException(errorCode: ErrorCode, message: String? = null) : AlignlabException(errorCode, message)

class FailToCreateException(errorCode: ErrorCode) : AlignlabException(errorCode)

Expand Down
3 changes: 3 additions & 0 deletions src/main/kotlin/com/hero/alignlab/exception/ErrorCode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ enum class ErrorCode(val status: HttpStatus, val description: String) {
INVALID_OAUTH_PROVIDER(HttpStatus.BAD_REQUEST, "잘못된 oauth 벤더입니다."),
DUPLICATED_USERNAME_ERROR(HttpStatus.BAD_REQUEST, "중복된 아이디 입니다."),

/** OAuth Error Code */
NOT_FOUND_OAUTH_PROVIDER_ERROR(HttpStatus.NOT_FOUND, "현재 미지원하는 제공자입니다."),

/** User Error Code */
NOT_FOUND_USER_ERROR(HttpStatus.NOT_FOUND, "유저 정보를 찾을 수 없습니다."),

Expand Down
9 changes: 9 additions & 0 deletions src/main/resources/config/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,15 @@ auth:
jwt:
secret:

oauth:
kakao:
url: https://kauth.kakao.com/oauth
rest-api-key:
client-secret-code:
# redirect url for get OAuth Authorize Code
redirect-url: http://localhost:3000
dev-redirect-url: https://api.alignlab.site/api/dev/v1/oauth/kakao/authorize/redirected

encrypt:
key:
algorithm:
Expand Down

0 comments on commit 3aeac01

Please sign in to comment.