[Android] 구글 로그인

 

구현 기능 

최대한 쉽고 간편하게 SSO 로그인을 구현하는 과정을 정리해보려고 합니다.

앱을 개발할 때 카카오, 구글 로그인을 대부분 적용하게 되었는데요.

매번 구글링하고, 급하게 기능을 구현하다보니 정리하는 과정이 필요할 것 같아서 포스팅하게 되었습니다.

 

의존성과 라이브러리 추가 

buildscript {
    repositories {
        google()
        mavenCentral()
    }
}

 

프로젝트 수준 build.gradle 파일에 Google의 Maven 저장소를 추가합니다.

    // Google Play services
    implementation 'com.google.gms:google-services:4.3.15'
    implementation 'com.google.firebase:firebase-auth:22.0.0'
    implementation 'com.google.firebase:firebase-bom:32.0.0'
    implementation 'com.google.android.gms:play-services-auth:20.5.0'

 

앱 수준 build.gradle에 라이브러리를 추가합니다.

저는 멀티모듈 구조를 활용하고 있어서 login 모듈에 기능을 추가하였습니다.

 

구글 클라우드 콘솔 프로젝트 생성

 

https://console.cloud.google.com/

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

 

구글 로그인 기능을 구현하기 위해서 구글 클라우드 콘솔 프로젝트를 생성해야 합니다.

 

새 프로젝트를 통해서 프로젝트를 생성합니다. 

조직은 별도로 지정하지 않아도 되고, 적절한 이름을 입력해서 생성하면 됩니다 !

 

앱 패키지명과 SHA-1 해시값 로드 

프로젝트를 등록하기 위해서 패키지명과 암호화 된 해시값이 필요합니다.

 

패키지명의 경우 앱 수준의 build.gradle에서 확인할 수 있습니다.

 

SHA-1 해시값은 우측의 Gradle(코끼리 아이콘)을 클릭한 후  gradle signingReport 입력을 통해 확인할 수 있습니다.

 

터미널에 해쉬값이 출력 됩니다.

 

OAuth 2.0 클라이언트 ID 로드

이제 구글 콘솔에서 클라이언트 ID로 사용자 인증 정보를 생성해야 합니다.

상단에 검색 기능을 통해서 사용자 인증을 검색한 후, 사용자 인증 정보 만들기를 생성합니다.

 

OAuth 동의 화면을 생성해야 하는데, 앱을 출시하기 위해서 외부로 설정합니다.

 

패키지 명과 SHA-1 인증서 디지털 지문 입력란에 아까 받아온 SHA-1키를 적용합니다.

 

클라이언트 아이디를 성공적으로 발급받을 수 있습니다.

여기서 얻은 클라이언트 ID를 GoogleSignInOptions 객체를 만들 때 토큰이나 코드에 사용하게 됩니다.

Firebase 프로젝트 생성

구글 클라우드 콘솔에 프로젝트를 추가한 경우, 파이어베이스에서 연동할 수 있습니다.

프로젝트 생성을 클릭하면 아래와 같이 콘솔에서 생성한 프로젝트를 자동으로 매칭해줍니다.

 

프로젝트 생성을 완료하였습니다 .!

 

프로젝트 생성을 완료하면 google-services.json을 다운로드 하라는 안내를 받을 수 있습니다.

다운로드 받은 json 파일을 프로젝트에 추가하여 빌드하면 됩니다.

 

로그인 모듈에 클라이언트 ID가 담긴 json 파일을 추가하였습니다.

처음 파일을 다운받으면 파일명이 클라이언트 ID로 되어있기 때문에 파일명을 변경해 주었습니다.

 

안드로이드 연동

기능을 구현하기 위한 GoogleLoginRepository를 생성하였습니다.

Repository에는 총 3가지 기능이 있습니다.

ActivityResult Launcher를 생성하는 setLauncher,

구글 서버로 로그인 요청을 보내는 login,

백엔드 서버에 요청을 보내는 loginRequest가 있습니다.

 

플로우는 런처 생성 -> 구글 로그인 후 응답 -> 응답 값을 백엔드 서버로 전송하는 방식을 따릅니다.

 

class GoogleLoginRepository @Inject constructor(
    @ApplicationContext private val context: Context,
    private val networkManager: NetworkManager
) {

    fun setLauncher(
        result: ActivityResult,
        firebaseAuth: FirebaseAuth,
        loginCallback: (String?) -> Unit
    ) {
        var tokenId: String?
        var email: String
        if (result.resultCode == AppCompatActivity.RESULT_OK) {
            val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
            Log.d("task",task.toString())
            try {
                task.getResult(ApiException::class.java)?.let { account ->
                    tokenId = account.idToken
                    Log.d("tokenId",tokenId.toString())
                    if (tokenId != null && tokenId != "") {
                        val credential: AuthCredential =
                            GoogleAuthProvider.getCredential(account.idToken, null)

                        firebaseAuth.signInWithCredential(credential).addOnCompleteListener {
                            if (firebaseAuth.currentUser != null) {
                                val user: FirebaseUser = firebaseAuth.currentUser!!
                                email = user.email.toString()
                                Log.e(ContentValues.TAG, "email : $email")
                                val googleSignInToken = account.idToken ?: ""
                                if (googleSignInToken != "") {
                                    loginCallback(googleSignInToken)
                                } else {
                                    loginCallback(null)
                                }
                            } else {
                                loginCallback(null)
                            }
                        }
                    }
                } ?: throw  Exception()
            } catch (e: Exception) {
                e.printStackTrace()
                loginCallback(null)
            }
        } else {
            loginCallback(null)
        }
    }

    fun login(
        clientId: String,
        launcher: ActivityResultLauncher<Intent>
    ) {
        CoroutineScope(Dispatchers.IO).launch {
            val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestIdToken(clientId)
                .requestEmail()
                .build()
            val googleSignInClient = GoogleSignIn.getClient(context, gso)
            val signInIntent: Intent = googleSignInClient.signInIntent
            launcher.launch(signInIntent)
        }
    }

    suspend fun loginRequest(idToken: String, result: (Boolean) -> Unit) {
        val userData = UserInfoSharedPreferences(context)
        userData.loginForm = NetworkManager.LOGIN_GOOGLE
        val response = networkManager.getLoginApiService().loginGoogle(idToken)
        if (response.isSuccessful) {
            result(true)
        } else {
            result(false)
        }
    }

 

비동기 처리로 데이터가 전송되기 때문에 callback 함수를 활용해서 데이터를 받으면 처리하는 형식을 적용하였습니다.

viewModel에서 Repository의 기능을 수행하는 코드를 작성하고, compose 화면에서 구글 로그인을 구현하였습니다.

    val loginUserState = remember { mutableStateOf(false) }
    val isGoogleLogin = remember { mutableStateOf(false) }
    val firebaseAuth = FirebaseAuth.getInstance()
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.StartActivityForResult(),
        onResult = { result ->
            viewModel.setLauncher(result,firebaseAuth, loginState = {state ->
                Log.d("loginRequest",state.toString())
                if (!state) isGoogleLogin.value = false
                loginUserState.value = state
            })
        }
    )

    if (isGoogleLogin.value){
        viewModel.goggleLogin(launcher)
    }

 

사용자가 로그인 버튼을 클릭하게 되면 isGoogleLogin을 true 상태로 전환합니다.

이 후에 액티비티 result를 받는 launcher와 firebaseAuth를 생성하여 기능을 적용합니다.

로그인에 실패할 경우 isGoogleLogin을 false 상태로 전환하여 사용자 로그인을 취소하였고,

성공할 경우 loginUserState를 true로 전환하여 다음 액티비티로 이동하도록 하였습니다.