구글 로그인 추가
This commit is contained in:
parent
1bbaf8f7b7
commit
5598bca8d3
|
@ -65,6 +65,8 @@ dependencies {
|
||||||
// android publisher
|
// android publisher
|
||||||
implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20240319-2.0.0")
|
implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20240319-2.0.0")
|
||||||
|
|
||||||
|
implementation("com.google.api-client:google-api-client:1.32.1")
|
||||||
|
|
||||||
implementation("org.apache.poi:poi-ooxml:5.2.3")
|
implementation("org.apache.poi:poi-ooxml:5.2.3")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,13 @@ data class Member(
|
||||||
var nickname: String,
|
var nickname: String,
|
||||||
var profileImage: String? = null,
|
var profileImage: String? = null,
|
||||||
|
|
||||||
|
val kakaoId: Long? = null,
|
||||||
|
val googleId: String? = null,
|
||||||
|
val appleId: String? = null,
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
val provider: MemberProvider = MemberProvider.EMAIL,
|
||||||
|
|
||||||
@Enumerated(value = EnumType.STRING)
|
@Enumerated(value = EnumType.STRING)
|
||||||
var gender: Gender = Gender.NONE,
|
var gender: Gender = Gender.NONE,
|
||||||
|
|
||||||
|
@ -140,3 +147,7 @@ enum class Gender {
|
||||||
enum class MemberRole {
|
enum class MemberRole {
|
||||||
ADMIN, BOT, USER, CREATOR, AGENT
|
ADMIN, BOT, USER, CREATOR, AGENT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class MemberProvider {
|
||||||
|
EMAIL, KAKAO, GOOGLE, APPLE
|
||||||
|
}
|
||||||
|
|
|
@ -8,8 +8,10 @@ import kr.co.vividnext.sodalive.member.block.MemberBlockRequest
|
||||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest
|
import kr.co.vividnext.sodalive.member.following.CreatorFollowRequest
|
||||||
import kr.co.vividnext.sodalive.member.login.LoginRequest
|
import kr.co.vividnext.sodalive.member.login.LoginRequest
|
||||||
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||||
|
import kr.co.vividnext.sodalive.member.login.SocialLoginRequest
|
||||||
import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest
|
import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest
|
||||||
import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
|
import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
|
||||||
|
import kr.co.vividnext.sodalive.member.social.google.GoogleAuthService
|
||||||
import org.springframework.data.domain.Pageable
|
import org.springframework.data.domain.Pageable
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
import org.springframework.security.core.userdetails.User
|
import org.springframework.security.core.userdetails.User
|
||||||
|
@ -29,6 +31,7 @@ import org.springframework.web.multipart.MultipartFile
|
||||||
@RequestMapping("/member")
|
@RequestMapping("/member")
|
||||||
class MemberController(
|
class MemberController(
|
||||||
private val service: MemberService,
|
private val service: MemberService,
|
||||||
|
private val googleAuthService: GoogleAuthService,
|
||||||
private val trackingService: AdTrackingService
|
private val trackingService: AdTrackingService
|
||||||
) {
|
) {
|
||||||
@GetMapping("/check/email")
|
@GetMapping("/check/email")
|
||||||
|
@ -316,4 +319,27 @@ class MemberController(
|
||||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
ApiResponse.ok(service.getMemberProfile(memberId = id, myMemberId = member.id!!))
|
ApiResponse.ok(service.getMemberProfile(memberId = id, myMemberId = member.id!!))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/login/google")
|
||||||
|
fun loginGoogle(
|
||||||
|
@RequestHeader("Authorization") authHeader: String,
|
||||||
|
@RequestBody request: SocialLoginRequest
|
||||||
|
): ApiResponse<LoginResponse> {
|
||||||
|
if (!authHeader.startsWith("Bearer ")) {
|
||||||
|
throw SodaException("구글 로그인을 하지 못했습니다. 다시 시도해 주세요")
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = authHeader.substring(7)
|
||||||
|
val response = googleAuthService.authenticate(token, request.container, request.marketingPid)
|
||||||
|
|
||||||
|
if (!response.marketingPid.isNullOrBlank()) {
|
||||||
|
trackingService.saveTrackingHistory(
|
||||||
|
pid = response.marketingPid,
|
||||||
|
type = AdTrackingHistoryType.SIGNUP,
|
||||||
|
memberId = response.memberId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse.ok(message = "회원가입을 축하드립니다.", data = response.loginResponse)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import org.springframework.stereotype.Repository
|
||||||
interface MemberRepository : JpaRepository<Member, Long>, MemberQueryRepository {
|
interface MemberRepository : JpaRepository<Member, Long>, MemberQueryRepository {
|
||||||
fun findByEmail(email: String): Member?
|
fun findByEmail(email: String): Member?
|
||||||
fun findByNickname(nickname: String): Member?
|
fun findByNickname(nickname: String): Member?
|
||||||
|
fun findByGoogleId(googleId: String): Member?
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MemberQueryRepository {
|
interface MemberQueryRepository {
|
||||||
|
|
|
@ -32,6 +32,7 @@ import kr.co.vividnext.sodalive.member.signUp.SignUpRequest
|
||||||
import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
|
import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
|
||||||
import kr.co.vividnext.sodalive.member.signUp.SignUpResponse
|
import kr.co.vividnext.sodalive.member.signUp.SignUpResponse
|
||||||
import kr.co.vividnext.sodalive.member.signUp.SignUpValidator
|
import kr.co.vividnext.sodalive.member.signUp.SignUpValidator
|
||||||
|
import kr.co.vividnext.sodalive.member.social.google.GoogleUserInfo
|
||||||
import kr.co.vividnext.sodalive.member.stipulation.Stipulation
|
import kr.co.vividnext.sodalive.member.stipulation.Stipulation
|
||||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree
|
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree
|
||||||
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
|
import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository
|
||||||
|
@ -736,4 +737,36 @@ class MemberService(
|
||||||
|
|
||||||
return member.activePid
|
return member.activePid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun findOrRegister(googleUserInfo: GoogleUserInfo, container: String, marketingPid: String?): Member {
|
||||||
|
repository.findByGoogleId(googleUserInfo.sub)?.let { return it }
|
||||||
|
|
||||||
|
val stipulationTermsOfService = stipulationRepository.findByIdOrNull(StipulationIds.TERMS_OF_SERVICE_ID)
|
||||||
|
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
|
||||||
|
|
||||||
|
val stipulationPrivacyPolicy = stipulationRepository.findByIdOrNull(StipulationIds.PRIVACY_POLICY_ID)
|
||||||
|
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
|
||||||
|
|
||||||
|
val nickname = nicknameGenerateService.generateUniqueNickname()
|
||||||
|
val member = Member(
|
||||||
|
googleId = googleUserInfo.sub,
|
||||||
|
email = googleUserInfo.email,
|
||||||
|
password = "",
|
||||||
|
nickname = nickname,
|
||||||
|
profileImage = "profile/default-profile.png",
|
||||||
|
gender = Gender.NONE,
|
||||||
|
container = container
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!marketingPid.isNullOrBlank()) {
|
||||||
|
member.activePid = marketingPid
|
||||||
|
member.partnerExpirationDatetime = LocalDateTime.now().plusYears(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.save(member)
|
||||||
|
agreeTermsOfServiceAndPrivacyPolicy(member, stipulationTermsOfService, stipulationPrivacyPolicy)
|
||||||
|
|
||||||
|
return member
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,3 +6,5 @@ data class LoginRequest(
|
||||||
val isAdmin: Boolean = false,
|
val isAdmin: Boolean = false,
|
||||||
val isCreator: Boolean = false
|
val isCreator: Boolean = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SocialLoginRequest(val container: String, val marketingPid: String? = null)
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.social
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||||
|
|
||||||
|
data class SocialLoginResponse(
|
||||||
|
val memberId: Long,
|
||||||
|
val marketingPid: String?,
|
||||||
|
val loginResponse: LoginResponse
|
||||||
|
)
|
|
@ -0,0 +1,54 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.social.google
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.jwt.TokenProvider
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberService
|
||||||
|
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||||
|
import kr.co.vividnext.sodalive.member.social.SocialLoginResponse
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class GoogleAuthService(
|
||||||
|
private val googleService: GoogleService,
|
||||||
|
private val memberService: MemberService,
|
||||||
|
private val tokenProvider: TokenProvider,
|
||||||
|
|
||||||
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
private val cloudFrontHost: String
|
||||||
|
) {
|
||||||
|
fun authenticate(idToken: String, container: String, marketingPid: String?): SocialLoginResponse {
|
||||||
|
val googleUserInfo = googleService.getUserInfo(idToken)
|
||||||
|
?: throw SodaException("구글 로그인을 하지 못했습니다. 다시 시도해 주세요")
|
||||||
|
val member = memberService.findOrRegister(googleUserInfo, container, marketingPid)
|
||||||
|
val principal = MemberAdapter(member)
|
||||||
|
val authToken = GoogleAuthenticationToken(idToken, principal.authorities)
|
||||||
|
authToken.setPrincipal(principal)
|
||||||
|
SecurityContextHolder.getContext().authentication = authToken
|
||||||
|
|
||||||
|
val jwt = tokenProvider.createToken(
|
||||||
|
authentication = authToken,
|
||||||
|
memberId = member.id!!
|
||||||
|
)
|
||||||
|
|
||||||
|
val loginResponse = LoginResponse(
|
||||||
|
userId = member.id!!,
|
||||||
|
token = jwt,
|
||||||
|
nickname = member.nickname,
|
||||||
|
email = member.email,
|
||||||
|
profileImage = if (member.profileImage != null) {
|
||||||
|
"$cloudFrontHost/${member.profileImage}"
|
||||||
|
} else {
|
||||||
|
"$cloudFrontHost/profile/default-profile.png"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return SocialLoginResponse(
|
||||||
|
memberId = member.id!!,
|
||||||
|
marketingPid = marketingPid,
|
||||||
|
loginResponse = loginResponse
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.social.google
|
||||||
|
|
||||||
|
import org.springframework.security.authentication.AbstractAuthenticationToken
|
||||||
|
import org.springframework.security.core.GrantedAuthority
|
||||||
|
|
||||||
|
class GoogleAuthenticationToken(
|
||||||
|
private val idToken: String,
|
||||||
|
authorities: Collection<GrantedAuthority>? = null
|
||||||
|
) : AbstractAuthenticationToken(authorities) {
|
||||||
|
private var principal: Any? = null
|
||||||
|
init { isAuthenticated = authorities != null }
|
||||||
|
override fun getCredentials(): Any = idToken
|
||||||
|
override fun getPrincipal(): Any? = principal
|
||||||
|
fun setPrincipal(principal: Any) { this.principal = principal }
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.social.google
|
||||||
|
|
||||||
|
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier
|
||||||
|
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
|
||||||
|
import com.google.api.client.json.gson.GsonFactory
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class GoogleService(
|
||||||
|
@Value("\${google.web-client-id}")
|
||||||
|
private val googleWebClientId: String
|
||||||
|
) {
|
||||||
|
private val transport = GoogleNetHttpTransport.newTrustedTransport()
|
||||||
|
private val jsonFactory = GsonFactory.getDefaultInstance()
|
||||||
|
private val clientIds = listOf(googleWebClientId)
|
||||||
|
|
||||||
|
fun getUserInfo(idToken: String): GoogleUserInfo? {
|
||||||
|
val verifier = GoogleIdTokenVerifier.Builder(transport, jsonFactory)
|
||||||
|
.setAudience(clientIds)
|
||||||
|
.setIssuers(listOf("accounts.google.com", "https://accounts.google.com"))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val token = verifier.verify(idToken)
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
val payload = token.payload
|
||||||
|
val email = payload.email ?: throw SodaException("이메일 제공에 동의하셔야 서비스 이용이 가능합니다.")
|
||||||
|
|
||||||
|
GoogleUserInfo(
|
||||||
|
sub = payload.subject,
|
||||||
|
email = email,
|
||||||
|
name = payload["name"] as? String
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package kr.co.vividnext.sodalive.member.social.google
|
||||||
|
|
||||||
|
data class GoogleUserInfo(
|
||||||
|
val sub: String,
|
||||||
|
val email: String,
|
||||||
|
val name: String?
|
||||||
|
)
|
|
@ -25,6 +25,9 @@ agora:
|
||||||
firebase:
|
firebase:
|
||||||
secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS}
|
secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS}
|
||||||
|
|
||||||
|
google:
|
||||||
|
webClientId: ${GOOGLE_WEB_CLIENT_ID}
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
aws:
|
aws:
|
||||||
credentials:
|
credentials:
|
||||||
|
|
Loading…
Reference in New Issue