Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: Receive Kakao Server Access Token (OAuth2) #5

Merged
merged 9 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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: 1 addition & 1 deletion CONFIG
67 changes: 37 additions & 30 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,58 +1,65 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("org.springframework.boot") version "3.2.3"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.22"
kotlin("plugin.spring") version "1.9.22"
kotlin("plugin.jpa") version "1.9.22"
kotlin("plugin.allopen") version "1.9.22"
id("org.springframework.boot") version "3.2.3"
id("io.spring.dependency-management") version "1.1.4"
kotlin("jvm") version "1.9.23"
kotlin("plugin.spring") version "1.9.23"
kotlin("plugin.jpa") version "1.9.23"
kotlin("plugin.allopen") version "1.9.23"
}

group = "com.vacgom"
version = "0.0.1-SNAPSHOT"

java {
sourceCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")

implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.flywaydb:flyway-core")
implementation("org.flywaydb:flyway-mysql")
implementation("org.jetbrains.kotlin:kotlin-reflect")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.flywaydb:flyway-core")
implementation("org.flywaydb:flyway-mysql")
implementation("org.jetbrains.kotlin:kotlin-reflect")
developmentOnly("org.springframework.boot:spring-boot-devtools")
runtimeOnly("com.mysql:mysql-connector-j")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.springframework.security:spring-security-test")
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
implementation("io.jsonwebtoken:jjwt-gson:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}

tasks.withType<Test> {
useJUnitPlatform()
useJUnitPlatform()
}

tasks.processResources {
dependsOn("initConfig")
dependsOn("initConfig")
}

tasks.register<Copy>("initConfig") {
from("./CONFIG")
include("*.yml")
into("./src/main/resources")
from("./CONFIG")
include("*.yml")
into("./src/main/resources")
}

allOpen {
annotation("jakarta.persistence.Entity")
}
30 changes: 30 additions & 0 deletions src/main/kotlin/com/vacgom/backend/application/auth/AuthFactory.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.vacgom.backend.application.auth

import com.vacgom.backend.domain.auth.oauth.OauthConnector
import com.vacgom.backend.domain.auth.oauth.OauthUriGenerator
import com.vacgom.backend.domain.auth.oauth.constants.ProviderType
import com.vacgom.backend.global.exception.error.BusinessException
import com.vacgom.backend.global.security.exception.AuthError
import org.springframework.stereotype.Component

@Component
class AuthFactory(
private val connectors: List<OauthConnector>,
private val uriProviders: List<OauthUriGenerator>
) {
fun getAuthConnector(provider: String): OauthConnector {
val providerType = ProviderType.from(provider)

return connectors.firstOrNull {
it.isSupported(providerType)
} ?: throw BusinessException(AuthError.UNSUPPORTED_PROVIDER)
}

fun getAuthUriGenerator(provider: String): OauthUriGenerator {
val providerType = ProviderType.from(provider)

return uriProviders.firstOrNull {
it.isSupported(providerType)
} ?: throw BusinessException(AuthError.UNSUPPORTED_PROVIDER)
}
}
57 changes: 57 additions & 0 deletions src/main/kotlin/com/vacgom/backend/application/auth/AuthService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.vacgom.backend.application.auth

import com.vacgom.backend.application.auth.dto.LoginResponse
import com.vacgom.backend.application.auth.dto.MemberResponse
import com.vacgom.backend.domain.auth.constants.Role
import com.vacgom.backend.domain.auth.oauth.constants.ProviderType
import com.vacgom.backend.domain.member.Member
import com.vacgom.backend.global.security.jwt.JwtTokenManager
import com.vacgom.backend.infrastructure.member.persistence.MemberRepository
import jakarta.transaction.Transactional
import org.springframework.http.HttpHeaders
import org.springframework.stereotype.Component
import java.net.URI

@Component
class AuthService(
private val authFactory: AuthFactory,
private val jwtTokenManager: JwtTokenManager,
private val memberRepository: MemberRepository
) {
fun createRedirectHeaders(redirectUri: URI): HttpHeaders {
val headers = HttpHeaders()
headers.location = redirectUri
return headers
}

fun getAuthorizationUri(provider: String): URI {
return authFactory.getAuthUriGenerator(provider).generate()
}

@Transactional
fun login(
providerType: String,
code: String
): LoginResponse {
val authConnector = authFactory.getAuthConnector(providerType)
val oauthToken = authConnector.fetchOauthToken(code)
val memberInfo = authConnector.fetchMemberInfo(oauthToken.accessToken)
val member = findOrCreateMember(memberInfo.id, ProviderType.from(providerType))

val memberResponse = MemberResponse(member.id!!, member.role)
val tokenResponse = jwtTokenManager.generate(member.id!!)

return LoginResponse(memberResponse, tokenResponse)
}

private fun findOrCreateMember(
kakaoProviderId: Long,
providerType: ProviderType
): Member {
val existingMember = memberRepository.findByProviderIdAndProviderType(kakaoProviderId, providerType)
h-beeen marked this conversation as resolved.
Show resolved Hide resolved
return existingMember ?: run {
val newMember = Member(kakaoProviderId, providerType, Role.ROLE_TEMP_USER)
memberRepository.save(newMember)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.vacgom.backend.application.auth.dto

data class KakaoMemberResponse(
var id: Long
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.vacgom.backend.application.auth.dto

data class LoginResponse(
val member: MemberResponse,
val token: TokenResponse
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.vacgom.backend.application.auth.dto

import com.vacgom.backend.domain.auth.constants.Role
import java.util.*

data class MemberResponse(
val memberId: UUID,
val role: Role
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.vacgom.backend.application.auth.dto

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

@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy::class)
data class OauthTokenResponse(val accessToken: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.vacgom.backend.application.auth.dto


data class TokenResponse(
val accessToken: String,
val refreshToken: String
)
23 changes: 23 additions & 0 deletions src/main/kotlin/com/vacgom/backend/domain/auth/RefreshToken.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.vacgom.backend.domain.auth

import com.vacgom.backend.domain.member.Member
import jakarta.persistence.*
import java.time.LocalDateTime


@Entity
@Table(name = "t_refresh_token")
class RefreshToken(
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false)
var member: Member,
var token: String,
var userAgent: String,
var expiredAt: LocalDateTime
) {
@Id
@GeneratedValue
@Column(name = "rt_id")
private val id: Long? = null
Copy link
Collaborator

Choose a reason for hiding this comment

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

id๋„ ๋‹ค๋ฅธ ํ”„๋ผํผํ‹ฐ๋“ค ์ฒ˜๋Ÿผ ์ƒ์„ฑ์ž ์ž๋ฆฌ์—์„œ ์ •์˜ํ•˜์…”๋„ ๋ฌธ์ œ ์—†์Šต๋‹ˆ๋‹ค !

Copy link
Collaborator

Choose a reason for hiding this comment

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

๊ทธ๋ฆฌ๊ณ  RefreshToken ์—”ํ‹ฐํ‹ฐ๋Š” BaseEntity ์ƒ์†์„ ์•ˆ๋ฐ›๊ณ  ์žˆ๋Š”๋ฐ ์˜๋„๋œ ๊ฒƒ์ธ์ง€๋„ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค !!

Copy link
Member Author

@h-beeen h-beeen Mar 17, 2024

Choose a reason for hiding this comment

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

@HyungJu ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค!!
์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด RefreshToken์„ ์ƒ์„ฑํ•˜๋ ค๊ณ  ํ•  ๋•Œ,
id์™€ ๊ฐ™์€ GeneratedValue Property๋ฅผ null๋กœ ํ• ๋‹นํ•ด ์ž‘์—…ํ•˜๋‚˜์š”?
๊ฐ์ฒด ์ƒ์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ์— null์„ ๋„ฃ๋Š”๊ฒŒ ์–ด์ƒ‰ํ•ด์„œ ๋ถ„๋ฆฌํ•ด๋ณด์•˜๋Š”๋ฐ, ์š” ๋ถ€๋ถ„๋„ ์˜๊ฒฌ ๋ถ€ํƒ๋“œ๋ฆด๊ฒŒ์š”!!

Copy link
Member Author

@h-beeen h-beeen Mar 17, 2024

Choose a reason for hiding this comment

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

๊ทธ๋ฆฌ๊ณ  RefreshToken ์—”ํ‹ฐํ‹ฐ๋Š” BaseEntity ์ƒ์†์„ ์•ˆ๋ฐ›๊ณ  ์žˆ๋Š”๋ฐ ์˜๋„๋œ ๊ฒƒ์ธ์ง€๋„ ๊ถ๊ธˆํ•ฉ๋‹ˆ๋‹ค !!

RefreshToken์—ญ์‹œ BaseEntity ์ƒ์†์„ ๋ฐ›์•„์•ผ ๋งž์Šต๋‹ˆ๋‹ค!
์š” ๋ถ€๋ถ„๋„ ๋ฐ˜์˜ํ•ด๋‘˜๊ฒŒ์š”!!
p.s.Redis ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์—ผ๋‘์ค‘..

Copy link
Collaborator

@HyungJu HyungJu Mar 17, 2024

Choose a reason for hiding this comment

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

@HyungJu ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค!! ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด RefreshToken์„ ์ƒ์„ฑํ•˜๋ ค๊ณ  ํ•  ๋•Œ, id์™€ ๊ฐ™์€ GeneratedValue Property๋ฅผ null๋กœ ํ• ๋‹นํ•ด ์ž‘์—…ํ•˜๋‚˜์š”? ๊ฐ์ฒด ์ƒ์„ฑ ํŒŒ๋ผ๋ฏธํ„ฐ์— null์„ ๋„ฃ๋Š”๊ฒŒ ์–ด์ƒ‰ํ•ด์„œ ๋ถ„๋ฆฌํ•ด๋ณด์•˜๋Š”๋ฐ, ์š” ๋ถ€๋ถ„๋„ ์˜๊ฒฌ ๋ถ€ํƒ๋“œ๋ฆด๊ฒŒ์š”!!

์‚ฌ์‹ค ์ „ Domain Model -> Entity ๊ฐ„ ๋ณ€ํ™˜์„ ํ• ๋•Œ id๋„ ํ•จ๊ป˜ ๋งคํ•‘์„ ํ•ด์ค˜์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— .. ๊ทธ๋ ‡๊ฒŒ ํ•˜๊ณ  ์žˆ์–ด์š”
image
(์ด๋ฏธ์ง€ ์ฐธ์กฐ)

์•„๋‹ˆ๋ฉด named argument๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ์กฐ๊ธˆ ๋ณด๊ธฐ ์ข‹๊ฒŒ ํ‘œํ˜„ํ• ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค ! (named argument๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด default value๊ฐ€ ์ง€์ •๋˜์–ด ์žˆ์„๋•Œ ํ•ด๋‹น argument๋Š” ์ƒ๋žตํ•  ์ˆ˜ ์žˆ์–ด์š”)
image

Copy link
Member Author

Choose a reason for hiding this comment

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

์˜คํ˜ธ ์ด ๋ถ€๋ถ„์€ ์ฒ˜์Œ ์ ‘ํ•˜๋Š” ์ •๋ณด์ธ๋ฐ์š”..
์–ด๋–ค ๋Š๋‚Œ์ธ์ง€๋Š” ์•Œ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค!
์š” ๋ถ€๋ถ„๋„ #8 PR์—์„œ ๋ฐ˜์˜ํ•ด๋ณผ๊ฒŒ์š”!!

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.vacgom.backend.domain.auth.constants

enum class Role {
ROLE_TEMP_USER,
ROLE_USER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.vacgom.backend.domain.auth.oauth

import com.vacgom.backend.application.auth.dto.KakaoMemberResponse
import com.vacgom.backend.application.auth.dto.OauthTokenResponse
import com.vacgom.backend.domain.auth.oauth.constants.ProviderType

interface OauthConnector {
fun isSupported(provider: ProviderType): Boolean
fun fetchOauthToken(code: String): OauthTokenResponse
fun fetchMemberInfo(accessToken: String): KakaoMemberResponse
h-beeen marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.vacgom.backend.domain.auth.oauth

import com.vacgom.backend.domain.auth.oauth.constants.ProviderType
import java.net.URI

interface OauthUriGenerator {
fun isSupported(provider: ProviderType): Boolean
fun generate(): URI
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.vacgom.backend.domain.auth.oauth.constants

import com.vacgom.backend.global.exception.error.BusinessException
import com.vacgom.backend.global.security.exception.AuthError

enum class ProviderType(val provider: String) {
KAKAO("kakao");

companion object {
fun from(provider: String): ProviderType {
return entries.firstOrNull { it.provider == provider }
?: throw BusinessException(AuthError.UNSUPPORTED_PROVIDER)
h-beeen marked this conversation as resolved.
Show resolved Hide resolved
}
}

fun isKakao(): Boolean {
return this == KAKAO
}
}
24 changes: 14 additions & 10 deletions src/main/kotlin/com/vacgom/backend/domain/member/Member.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
package com.vacgom.backend.domain.member

import com.vacgom.backend.domain.auth.constants.Role
import com.vacgom.backend.domain.auth.oauth.constants.ProviderType
import com.vacgom.backend.global.auditing.BaseEntity
import jakarta.persistence.*
import org.hibernate.annotations.GenericGenerator
import java.util.*

@Entity
@Table(name = "t_member")
class Member(nickname: String) : BaseEntity() {
class Member(
var providerId: Long,
@Enumerated(EnumType.STRING) var providerType: ProviderType,
@Enumerated(EnumType.STRING) var role: Role,
) : BaseEntity() {

@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name = "uuid2", strategy = "uuid2")
@Column(columnDefinition = "BINARY(16)", name = "member_id")
var id: UUID? = null

@Column(name = "nickname")
val nickname: String

init {
this.nickname = nickname
}
var name: String? = null
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.vacgom.backend.global.auditing

import com.fasterxml.jackson.annotation.JsonFormat
import jakarta.persistence.Column
import jakarta.persistence.EntityListeners
import jakarta.persistence.MappedSuperclass
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.time.LocalDateTime
import jakarta.persistence.Column

@MappedSuperclass
@EntityListeners(value = [AuditingEntityListener::class])
Expand All @@ -24,7 +24,7 @@ abstract class BaseEntity {
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd a HH:mm"
)
val createdDate: LocalDateTime? = null
open var createdDate: LocalDateTime? = null

@Column(
nullable = false,
Expand All @@ -36,5 +36,5 @@ abstract class BaseEntity {
shape = JsonFormat.Shape.STRING,
pattern = "yyyy-MM-dd a HH:mm"
)
val updatedDate: LocalDateTime? = null
open var updatedDate: LocalDateTime? = null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.vacgom.backend.global.config

import org.springframework.context.annotation.Configuration
import org.springframework.data.jpa.repository.config.EnableJpaAuditing

@Configuration
@EnableJpaAuditing
class AuditingConfig
15 changes: 15 additions & 0 deletions src/main/kotlin/com/vacgom/backend/global/config/LoggerConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.vacgom.backend.global.config

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class LoggerConfig {

@Bean
fun logger(): Logger {
return LoggerFactory.getLogger(this.javaClass)!!
h-beeen marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading
Loading