Sangil's blog

https://github.com/ChoiSangIl Admin

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)
}

전체 소스를 올린건 아니라서 그대로 따라하시면 오류가 날 수 있을텐데 중요한 로직들은 다 들어가 있으니 입맛데로 가져다 사용하시면 될거 같습니다~
쉽게 생각했는데.. 생각보다 시간이 걸려서 기록..! 

#KakaoLogin #SpringKotlin Kakao Login #kotlin vue kakao
REPLY