Spring kotlin 카카오 로그인 구현 with Vue DEV / WEB
2023-02-10 posted by sang12
카카오 로그인을 구현하는데 생각보다 시간이 걸려서 기록용으로 남겨 놓습니다 :)
백엔드는 스프링 환경에서 Feign을 적용하였고 헥사고날 아키텍처 구조로 설계되어 있습니다.
프론트는 Vue를 사용하였습니다.
카카오 앱 등록
https://developers.kakao.com/console/app
로그인 - 내 애플리케이션 - 애플리케이션 추가하기
앱키를 기억하세요!
Vue 설정
Vue 설정하는데 생각보다 애를 많이 먹었습니다. 위치를 잘 확인해주세요!
-index.html
<script src="//developers.kakao.com/sdk/js/kakao.js"></script>
-main.js
window.Kakao.init('{appKey}')
- KakaoLogin.vue
<template>
<div class="login">
<p>{{ accessToken }}</p>
<a @click="socialLogin">
<img src="@/assets/images/kakao/kakao_login_large_narrow.png"/>
</a>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'UserLogin',
data () {
return {
accessToken: ''
}
},
methods: {
socialLogin: function(){
var ref = this;
const scope = "profile_nickname,account_email"
window.Kakao.Auth.login({
scope,
success: () => {
ref.drinkyLogin();
},
fail: (err) => {
console.log(err)
alert("카카오 서비스 이용을 할 수 없습니다")
}
})
},
drinkyLogin: function(){
var params = { kakaoAccessToken: window.Kakao.Auth.getAccessToken() }
var ref = this;
axios.get( "/drinky/api/v1/kakao/login",{params})
.then((response) => {
ref.accessToken = response.data.value
})
.catch((error)=>{
console.log(error)
ref.kakaoLogout()
alert("서버가 아파요 ㅜㅜ")
})
},
kakaoLogout: function(){
if(window.Kakao.Auth.getAccessToken()){
window.Kakao.Auth.logout(function(){
console.log("logout...")
})
}
}
}
}
</script>
스프링 With Kotlin
중요한 부분만 전체 첨부를 하였고 인터페이스와 같은 경우는 확인만 할 수 있게 올려놨습니다. 중요한 코드만 확인해 주세요!
Core - Application Service
- KakaoLoginController.kt
package com.drinky.endpoint.kakao
import com.drinky.core.application.social.kakao.`in`.KakaoSocialLoginUseCase
import com.drinky.core.domain.model.user.SocialAccessToken
import com.drinky.core.domain.model.user.UserAccessToken
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class KakaoLoginController(
private val kakaoSocialLoginUseCase: KakaoSocialLoginUseCase
) {
@GetMapping("/drinky/api/v1/kakao/login")
fun kakaoLogin(kakaoSocialLoginRequest: KakaoSocialLoginRequest): UserAccessToken =
kakaoSocialLoginUseCase.login(SocialAccessToken(kakaoSocialLoginRequest.kakaoAccessToken))
}
data class KakaoSocialLoginRequest (
val kakaoAccessToken: String
)
-KakaoSocialLoginService.kt
Application Service에서 하는 역할은 Vue에서 카카오 로그인을 통해 나온 AccesToken이 Valid한지 체크하고
Valid하면 회원가입을 시킨 후 토큰을 발급해주는 역할을 하고 있습니다.
package com.drinky.core.application.social.kakao
import com.drinky.core.application.auth.GenerateTokenService
import com.drinky.core.application.social.kakao.`in`.KakaoSocialLoginUseCase
import com.drinky.core.application.social.kakao.out.KakaoLoadUserPort
import com.drinky.core.application.social.kakao.out.KakaoTokenValidationPort
import com.drinky.core.application.user.UserSignUpService
import com.drinky.core.domain.model.user.SocialAccessToken
import com.drinky.core.domain.model.user.SocialUser
import com.drinky.core.domain.model.user.User
import com.drinky.core.domain.model.user.UserAccessToken
class KaKaoSocialLoginService(
private val kakaoLoadUserPort: KakaoLoadUserPort,
private val kakaoTokenValidationPort: KakaoTokenValidationPort,
private val userSignUpService: UserSignUpService,
private val generateTokenService: GenerateTokenService
) : KakaoSocialLoginUseCase {
override fun login(token: SocialAccessToken): UserAccessToken {
if (kakaoTokenValidationPort.isValid(token)) {
val kakaoUser: SocialUser = kakaoLoadUserPort.getKaKaoUserInfo(token)
val user: User = userSignUpService.signUpFromSocial(kakaoUser)
kakaoTokenValidationPort.expireToken(token)
return generateTokenService.getAccessToken(user)
} else {
throw Exception("invalid token")
}
}
}
interface KakaoSocialLoginUseCase {
fun login(token: SocialAccessToken): UserAccessToken
}
interface KakaoLoadUserPort {
fun getKaKaoUserInfo(token: SocialAccessToken): SocialUser
}
interface KakaoTokenValidationPort {
fun isValid(token: SocialAccessToken): Boolean
fun expireToken(token: SocialAccessToken)
}
-UserSignUpService.kt
UserSignUpService 에서는 회원 가입을 합니다. 해당 포트들을 구현한 어뎁터는 jpa 를 이용해서 구현하였습니다.
중요한 부분은 아닌거 같아서 각자 데이터베이스 환경에 맞게 인터페이스를 구현해주시면 되겠습니다.
class UserSignUpService(
private val saveUserPort: SaveUserPort,
private val loadUserPort: LoadUserPort
) : UserSignUpUseCase {
override fun signUpFromSocial(socialUser: SocialUser): User {
return try {
loadUserPort.findBySocialId(socialId = socialUser.getSocialId(), socialType = socialUser.socialType) ?: let {
saveUserPort.save(User(userRole = UserRole.USER, socialUser = socialUser))
}
} catch (exception: Exception) {
throw exception
}
}
}
interface SaveUserPort {
fun save(user : User): User
}
interface LoadUserPort {
fun loadUserById(id: Long): User?
fun findBySocialId(socialId : String, socialType: SocialType) : User?
}
data class User(
val id: Long = 0L,
val userRole: UserRole = UserRole.USER,
val socialUser: SocialUser
)
enum class UserRole {
USER, ADMIN
}
class UserAccessToken(
val value: String
)
GenerateTokenService.kt
각자의 서비스에 맞게 Token 생성을 해주는 로직을 구현해주면 되겠습니다. 저는 Jwt 토큰을 발급해주는 어댑터를 구현하였습니다.
class GenerateTokenService(
private val tokenProvider: TokenProvider
) {
fun getAccessToken(user: User): UserAccessToken = tokenProvider.getAccessToken(user)
}
카카오 회원 API With Feign
https://developers.kakao.com/tool/rest-api/open/post/v1-user-logout
Feign 설정방법에 대해선 설명하지 않습니다. 위 문서에 가면 카카오 Api 리스트를 볼 수 있습니다.
- KakaoExternalApi
package com.drinky.external
import com.drinky.core.application.social.kakao.out.KakaoLoadUserPort
import com.drinky.core.application.social.kakao.out.KakaoTokenValidationPort
import com.drinky.core.domain.model.user.SocialAccessToken
import com.drinky.core.domain.model.user.SocialType
import com.drinky.core.domain.model.user.SocialUser
import com.drinky.external.feign.KakaoProperty
import com.drinky.external.feign.KakaoTokenFeignClient
import com.drinky.external.feign.KakaoUserFeignClient
import com.drinky.external.feign.response.KakaoUserResponse
import org.springframework.stereotype.Component
@Component
class KakaoExternalApi(
val kakaoUserFeignClient: KakaoUserFeignClient,
val kakaoTokenFeignClient: KakaoTokenFeignClient
) : KakaoLoadUserPort, KakaoTokenValidationPort {
override fun getKaKaoUserInfo(token: SocialAccessToken): SocialUser {
val kakaoUserResponse: KakaoUserResponse =
kakaoUserFeignClient.getKakaoUser(
token.getBearerToken(),
KakaoProperty.toJson(KakaoProperty.PROFILE, KakaoProperty.EMAIL)
)
with(kakaoUserResponse) {
return SocialUser(id, SocialType.KAKAO, kakaoAccount?.profile?.nickname, kakaoAccount?.email)
}
}
override fun isValid(token: SocialAccessToken): Boolean {
return try {
kakaoTokenFeignClient.getTokenInfo(token.getBearerToken()).let { true }
} catch (e: Exception) {
false
}
}
override fun expireToken(token: SocialAccessToken) = kakaoTokenFeignClient.logout(token.getBearerToken())
}
data class SocialUser(
private val socialId: String,
val socialType: SocialType,
val nickName: String? = null,
val email: String? = null
) {
fun getSocialId() = "$socialId"
}
enum class SocialType {
KAKAO
}
class SocialAccessToken (
private val value: String
){
fun getBearerToken(): String = "${TokenType.BEARER.name} $value"
}
enum class TokenType {
BEARER
}
카카오 FeignClient
@FeignClient(name = "kakaoUserFeignClient", url = "https://kapi.kakao.com")
interface KakaoUserFeignClient {
@GetMapping("/v2/user/me")
fun getKakaoUser(@RequestHeader(name = "Authorization") Authorization: String, @RequestParam(name = "property_keys") propertyKey: String ): KakaoUserResponse
}
@FeignClient(name = "kakaoTokenFeignClient", url = "https://kapi.kakao.com")
interface KakaoTokenFeignClient {
@GetMapping("/v1/user/access_token_info")
fun getTokenInfo(@RequestHeader(name = "Authorization") Authorization: String): KakaoToeknInfo
@PostMapping("/v1/user/logout")
fun logout(@RequestHeader(name = "Authorization") Authorization: String)
}
전체 소스를 올린건 아니라서 그대로 따라하시면 오류가 날 수 있을텐데 중요한 로직들은 다 들어가 있으니 입맛데로 가져다 사용하시면 될거 같습니다~
쉽게 생각했는데.. 생각보다 시간이 걸려서 기록..!
1 | |
2024-11-01 12:34:45 |
1
답글