-
Notifications
You must be signed in to change notification settings - Fork 0
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
[YS-31] feat: 구글 OAuth 로그인 구현 #13
Changes from 15 commits
56683ae
d328163
b628066
2c55b09
7224c75
8389202
a90ec1b
167c73d
6d7ecbe
995bbfc
919cf9a
260fcbe
6f465bc
91ad19d
fe4ba57
d9598f6
cf8d7a2
85fa57f
eaa7a47
e558b11
0c10ed2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
package com.dobby.backend.application.mapper | ||
|
||
import com.dobby.backend.domain.exception.OAuth2EmailNotFoundException | ||
import com.dobby.backend.domain.exception.OAuth2NameNotFoundException | ||
import com.dobby.backend.infrastructure.database.entity.Member | ||
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus | ||
import com.dobby.backend.infrastructure.database.entity.enum.ProviderType | ||
import com.dobby.backend.presentation.api.dto.request.OauthUserDto | ||
import org.springframework.security.oauth2.core.user.OAuth2User | ||
|
||
object OauthUserMapper { | ||
fun toDto(oauthUser: OAuth2User, provider: ProviderType): OauthUserDto { | ||
val email = oauthUser.getAttribute<String>("email") | ||
?: throw OAuth2EmailNotFoundException() | ||
val name = oauthUser.getAttribute<String>("name") | ||
?: throw OAuth2NameNotFoundException() | ||
|
||
return OauthUserDto(email=email, name=name, provider= provider) | ||
} | ||
|
||
fun toTempMember(dto: OauthUserDto): Member { | ||
return Member( | ||
id = 0L, | ||
oauthEmail= dto.email, | ||
name= dto.name, | ||
provider = dto.provider, | ||
status = MemberStatus.HOLD, | ||
role = null, | ||
contactEmail = null, | ||
birthDate = null | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.dobby.backend.application.service | ||
|
||
import com.dobby.backend.domain.exception.MemberNotFoundException | ||
import com.dobby.backend.infrastructure.database.entity.Member | ||
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus | ||
import com.dobby.backend.infrastructure.database.repository.MemberRepository | ||
import com.dobby.backend.infrastructure.token.JwtTokenProvider | ||
import com.dobby.backend.presentation.api.dto.request.OauthUserDto | ||
import jakarta.transaction.Transactional | ||
import org.springframework.stereotype.Service | ||
|
||
@Service | ||
class MemberService( | ||
private val memberRepository: MemberRepository, | ||
private val jwtTokenProvider: JwtTokenProvider | ||
) { | ||
@Transactional | ||
fun login(oauthUserDto: OauthUserDto): Member{ | ||
val member= memberRepository.findByOauthEmailAndStatus(oauthUserDto.email, MemberStatus.ACTIVE) | ||
?: throw MemberNotFoundException() | ||
return member | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 제가 이해한 바로는, 클린 아키텍처에서 중요한 DIP을 지키기 위해서는 서비스 계층을 추상화하는 것이 올바른 접근이라고 생각합니다. 따라서, 다음 두 가지 방식 중에서 어떤 것이 더 적합할지 고민해볼 필요가 있을 거 같습니다.
수정님은 어떤 방향이 더 낫다고 생각하나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 1/2 서버 회의에서는 추상화를 철저히 하지만, 이번 주 주말 해커톤까지는 MVP를 빠르게 개발하는 것이 우선이므로, 추후 리팩터링을 통해 개선하기로 결정했습니다. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.dobby.backend.application.service | ||
|
||
import com.dobby.backend.application.usecase.FetchGoogleUserInfoUseCase | ||
import com.dobby.backend.domain.exception.OAuth2EmailNotFoundException | ||
import com.dobby.backend.domain.exception.OAuth2NameNotFoundException | ||
import com.dobby.backend.infrastructure.database.entity.enum.ProviderType | ||
import com.dobby.backend.presentation.api.dto.response.OauthTokenResponse | ||
import org.springframework.stereotype.Service | ||
import org.springframework.web.reactive.function.client.WebClient | ||
|
||
@Service | ||
class OauthService( | ||
private val fetchGoogleUserInfoUseCase: FetchGoogleUserInfoUseCase | ||
) { | ||
fun getGoogleUserInfo(accessToken: String): OauthTokenResponse { | ||
val userInfo = fetchGoogleUserInfoUseCase.execute(accessToken) | ||
|
||
val email = userInfo["email"] as? String ?: throw OAuth2EmailNotFoundException() | ||
val name = userInfo["name"] as? String ?: throw OAuth2NameNotFoundException() | ||
return OauthTokenResponse( | ||
jwtToken= accessToken, | ||
email = email, | ||
name = name, | ||
provider = ProviderType.GOOGLE | ||
) | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엔터 라인 한 번 쳐주세요~ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 반영하겠습니다! |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.dobby.backend.application.usecase | ||
|
||
import com.dobby.backend.domain.exception.OAuth2ProviderMissingException | ||
import org.springframework.stereotype.Component | ||
import org.springframework.web.reactive.function.client.WebClient | ||
import org.springframework.web.reactive.function.client.WebClientResponseException | ||
import org.springframework.web.reactive.function.client.bodyToMono | ||
|
||
@Component | ||
class FetchGoogleUserInfoUseCase( | ||
private val webClientBuilder : WebClient.Builder | ||
){ | ||
fun execute(accessToken: String) : Map<String, Any>{ | ||
try { | ||
val webClient = webClientBuilder.build() | ||
return webClient.get() | ||
.uri("https://www.googleapis.com/oauth2/v3/userinfo") | ||
.header("Authorization", "Bearer $accessToken") | ||
.retrieve() | ||
.bodyToMono<Map<String, Any>>() | ||
.block() ?: throw OAuth2ProviderMissingException() | ||
} catch (e : WebClientResponseException) { | ||
throw OAuth2ProviderMissingException() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
package com.dobby.backend.domain.exception | ||
|
||
open class OauthException ( | ||
errorCode: ErrorCode, | ||
) : DomainException(errorCode) | ||
class OAuth2AuthenticationException: OauthException(ErrorCode.OAUTH_USER_NOT_FOUND) | ||
class OAuth2ProviderMissingException: OauthException(ErrorCode.OAUTH_PROVIDER_MISSING) | ||
class OAuth2ProviderNotSupportedException: OauthException(ErrorCode.OAUTH_PROVIDER_NOT_FOUND) | ||
class OAuth2EmailNotFoundException : OauthException(ErrorCode.OAUTH_EMAIL_NOT_FOUND) | ||
class OAuth2NameNotFoundException : OauthException(ErrorCode.OAUTH_NAME_NOT_FOUND) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,14 +29,14 @@ open class Member ( | |
|
||
@Column(name = "role", nullable = true) | ||
@Enumerated(EnumType.STRING) | ||
val role: RoleType, | ||
val role: RoleType?, | ||
|
||
@Column(name = "contact_email", length = 100, nullable = false) | ||
val contactEmail : String, | ||
@Column(name = "contact_email", length = 100, nullable = true) | ||
val contactEmail : String?, | ||
|
||
@Column(name = "name", length = 10, nullable = false) | ||
val name : String, | ||
@Column(name = "name", length = 10, nullable = true) | ||
val name : String?, | ||
|
||
@Column(name = "birth_date", nullable = false) | ||
val birthDate : LocalDate, | ||
@Column(name = "birth_date", nullable = true) | ||
val birthDate : LocalDate?, | ||
) : AuditingEntity() | ||
Comment on lines
30
to
42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 기존 컬럼 규칙이 바뀌면 dev DB의 테이블도 이에 맞게 업데이트되어야 할 것 같습니다. 현재 DB에 데이터가 쌓이지 않은 상태라, 머지 전에 기존 테이블( |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
package com.dobby.backend.infrastructure.database.repository | ||
|
||
import com.dobby.backend.infrastructure.database.entity.Member | ||
import com.dobby.backend.infrastructure.database.entity.enum.MemberStatus | ||
import com.dobby.backend.infrastructure.database.entity.enum.ProviderType | ||
import org.springframework.data.jpa.repository.JpaRepository | ||
import org.springframework.stereotype.Repository | ||
|
||
@Repository | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사소한 부분이지만, JpaRepository를 상속받으면 자동으로 빈으로 등록되기 때문에 개인적으로는 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 명시성을 위해서였는데, |
||
interface MemberRepository : JpaRepository<Member, Long> { | ||
fun findByOauthEmailAndStatus(oauthEmail: String, status: MemberStatus): Member? | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package com.dobby.backend.infrastructure.database.repository | ||
|
||
import com.dobby.backend.infrastructure.database.entity.Participant | ||
import org.springframework.data.jpa.repository.JpaRepository | ||
import org.springframework.stereotype.Repository | ||
|
||
@Repository | ||
interface ParticipantRepository: JpaRepository<Participant, Long> { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,8 +6,7 @@ import io.swagger.v3.oas.annotations.info.Info | |
import io.swagger.v3.oas.annotations.servers.Server | ||
import io.swagger.v3.oas.models.Components | ||
import io.swagger.v3.oas.models.OpenAPI | ||
import io.swagger.v3.oas.models.security.SecurityRequirement | ||
import io.swagger.v3.oas.models.security.SecurityScheme | ||
import io.swagger.v3.oas.models.security.* | ||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
|
||
|
@@ -28,7 +27,33 @@ class SwaggerConfig { | |
@Bean | ||
fun openAPI(): OpenAPI = OpenAPI() | ||
.addSecurityItem(SecurityRequirement().addList("JWT 토큰")) | ||
.addSecurityItem(SecurityRequirement().addList("Google OAuth2 토큰")) | ||
.components( | ||
Components().addSecuritySchemes("JWT 토큰", | ||
SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("Bearer").bearerFormat("JWT"))) | ||
Components() | ||
.addSecuritySchemes( | ||
"JWT 토큰", | ||
SecurityScheme() | ||
.type(SecurityScheme.Type.HTTP) | ||
.scheme("Bearer") | ||
.bearerFormat("JWT") | ||
) | ||
.addSecuritySchemes( | ||
"Google OAuth2 토큰", | ||
SecurityScheme() | ||
.type(SecurityScheme.Type.OAUTH2) | ||
.flows( | ||
OAuthFlows() | ||
.authorizationCode( | ||
OAuthFlow() | ||
.authorizationUrl("https://accounts.google.com/o/oauth2/auth") | ||
.tokenUrl("https://oauth2.googleapis.com/token") | ||
.scopes( | ||
Scopes() | ||
.addString("email", "Access your email address") | ||
.addString("profile", "Access your profile information") | ||
) | ||
) | ||
) | ||
) | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
중첩된 부분을 위와 같이 별도 변수로 나누면 가독성이 좋아질 거 같습니다! 혹시 어떠신가요?? |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.dobby.backend.presentation.api.config | ||
|
||
import org.springframework.context.annotation.Bean | ||
import org.springframework.context.annotation.Configuration | ||
import org.springframework.web.reactive.function.client.WebClient | ||
|
||
@Configuration | ||
class WebClientConfig { | ||
@Bean | ||
fun webClientBuilder(): WebClient.Builder { | ||
return WebClient.builder() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
같은 dependency가 두 번 추가된 것 같습니다! 그리고 이제 FeignClient 라이브러리를 사용해서 구현할 예정인데, 이 라이브러리도 필요할지 궁금합니다.