Android

google login api(oauth2) 에 대한 고찰

jhg3410 2022. 1. 25. 11:39

진행 중인 프로젝트에서 google login을 구현하기로 결정되었다. 이를 구현하기 위한 노력들을 정리

 

간단하게 정리를 하자면 로직은 이렇다.

 

1. 구글에 로그인을 한다(GCP에 프로젝트 등록 후 구현 가능)

2. 그러면 각 계정에 맞는 auth_code값을 구글에서 전달한다.

3. auth_code값과 기타 중요 정보들을 다시 구글에 보내 access_token을 발급받는다.

4. access_token을 서버로 전달한다.

 

이 정도이다. 물론 결론이라 간단해 보이지만 이것들을 알아내는 데에도 많은 시간을 소요했다.

 

https://developers.google.com/identity/sign-in/android/start-integrating

 

Android 앱에 Google 로그인 통합 시작  |  Google Sign-In for Android  |  Google Developers

이 페이지는 Cloud Translation API를 통해 번역되었습니다. Switch to English Android 앱에 Google 로그인 통합 시작 자체 앱에서 Google 로그인 통합을 시작하려면 먼저 Google API 콘솔 프로젝트를 구성하고 Androi

developers.google.com

 

이 곳을 들어가보면 나와있는 로직은

 

1. 구글에 로그인(GCP에 프로젝트 등록 후 구현 가능)

2. 구글에서 id_token값을 발급

3. id_token값을 서버로 보내서 서버에서 작업

 

이지만 서버에서 access_token값을 원했기에 그냥 내가 구현해서 전달하기로 했다.

사실 위 google 문서에선 예제 코드들이 모두 java로 되어있어 직접 kotlin으로 변환해야 한다.

 

 

가장 많은 시간을 소요한 것은 access_token 발급이다.

https://stackoverflow.com/questions/33998335/how-to-get-access-token-after-user-is-signed-in-from-gmail-in-android

 

How to get access token after user is signed in from Gmail in Android?

I am following Google Sign in for Android. Now I can get the idToken but my back end server that I have used earlier is expecting access Token as I was using Google+ Login earlier. Now I don't want...

stackoverflow.com

위 질의응답이 해답이지만 나는 제대로 해석하지 않았고 auth_code값을 다른 방식으로 얻으려고 노력했던 시간이 크다..

사실 간단한 과정이였다. id_token값을 가져오는 것 처럼 그냥 auth_code를 가져오면 됐다.

 

전체적인 구현 로직을 설명하면

 

1. 안드로이드에서 구글 로그인을 위해 GCP 에 project가 필요하다

2. 안드로이드 사용자 인증 정보(OAuth 2.0 클라이언트 ID)가 필요하다. 물론 입력에 package명과 SHA-1 서명 인증서 지문이 필요하다. 

 

SHA-1 발급 참고https://modelmaker.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Debug-SHA-Key-%EC%B6%94%EC%B6%9C-%EB%B0%A9%EB%B2%95

위와 같이 생성.

3. 이제 위 구글 문서에서 처럼 구글 로그인 구현 가능

4. 구현해서 로그인 한 뒤 auth_code 발급 

5. GCP에  웹도 하나 만들어준다.

그러면 이제 위의 stackoverflow에 있는 것 처럼

구글로부터 access_token을 얻기 위한 client_id와 client_secret과 code값에 적을 친구들이 생겼다.

 

6, 구글에 post요청을 하면 access_token을 발급해준다!

 

만약 access_token이 아닌 id_token값이 필요하다면 구글 문서에 나와있는 데로 auth_code가 아닌 id_token을 가져오면 된다.

 

전체 코드 :

// LoginGoogle.kt

class LoginGoogle(context: Context) {
    private val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
        .requestIdToken(ClientInformation.CLIENT_ID)
        .requestServerAuthCode(ClientInformation.CLIENT_ID)
        .requestEmail()
        .build()

    private val googleSignInClient = GoogleSignIn.getClient(context, gso)

    fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
        try {
            val authCode: String? =
                completedTask.getResult(ApiException::class.java)?.serverAuthCode
            LoginRepository().getAccessToken(authCode!!)
        } catch (e: ApiException) {
            Log.w(TAG, "handleSignInResult: error" + e.statusCode)
        }
    }

    fun signIn(activity: Activity) {
        val signInIntent: Intent = googleSignInClient.signInIntent
        activity.startActivityForResult(signInIntent, 1000)

    }

    fun signOut(context: Context) {
        googleSignInClient.signOut()
            .addOnCompleteListener {
                Toast.makeText(context, "로그아웃 되셨습니다!", Toast.LENGTH_SHORT).show()
            }
    }

    fun isLogin(context: Context): Boolean {
        val account = GoogleSignIn.getLastSignedInAccount(context)
        return if (account == null) false else (true)
    }

    companion object {
        const val TAG = "GoogleLoginService"
    }
}

google문서와 비교하면서 확인하자.

 

// LoginRepository

class LoginRepository {

    private val getAccessTokenBaseUrl = "https://www.googleapis.com"
    private val sendAccessTokenBaseUrl = "server_base_url"

    fun getAccessToken(authCode:String) {
        LoginService.loginRetrofit(getAccessTokenBaseUrl).getAccessToken(
            request = LoginGoogleRequestModel(
                grant_type = "authorization_code",
                client_id = ClientInformation.CLIENT_ID,
                client_secret = ClientInformation.CLIENT_SECRET,
                code = authCode.orEmpty()
            )
        ).enqueue(object : Callback<LoginGoogleResponseModel> {
            override fun onResponse(call: Call<LoginGoogleResponseModel>, response: Response<LoginGoogleResponseModel>) {
                if(response.isSuccessful) {
                    val accessToken = response.body()?.access_token.orEmpty()
                    Log.d(TAG, "accessToken: $accessToken")
                    sendAccessToken(accessToken)
                }
            }
            override fun onFailure(call: Call<LoginGoogleResponseModel>, t: Throwable) {
                Log.e(TAG, "getOnFailure: ",t.fillInStackTrace() )
            }
        })
    }

    fun sendAccessToken(accessToken:String){
        LoginService.loginRetrofit(sendAccessTokenBaseUrl).sendAccessToken(
            accessToken = SendAccessTokenModel(accessToken)
        ).enqueue(object :Callback<String>{
            override fun onResponse(call: Call<String>, response: Response<String>) {
                if (response.isSuccessful){
                    Log.d(TAG, "sendOnResponse: ${response.body()}")
                }
            }
            override fun onFailure(call: Call<String>, t: Throwable) {
                Log.e(TAG, "sendOnFailure: ${t.fillInStackTrace()}", )
            }
        })
    }

    companion object {
        const val TAG = "LoginRepository"
    }
}

나중에 retrofit2를 사용할 때 코드가 너무 길어질 것 같아 나누었다.

sendAcessToken 은 서버에 전달하는 부분이니 getAccessToken 을 확인하고 각자 맞게 확인하면 될 듯하다.

 

// LoginService.kt

interface LoginService {
    @POST("oauth2/v4/token")
    fun getAccessToken(
        @Body request: LoginGoogleRequestModel
    ):
            Call<LoginGoogleResponseModel>


    @POST("login")
    @Headers("content-type: application/json")
    fun sendAccessToken(
        @Body accessToken:SendAccessTokenModel
    ):Call<String>

    companion object {

        private val gson = GsonBuilder().setLenient().create()

        fun loginRetrofit(baseUrl: String): LoginService {
            return Retrofit.Builder()
                .baseUrl(baseUrl)
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create(gson))
                .build()
                .create(LoginService::class.java)
        }
    }
}

api, retrofit을 할 때에 baseUrl이 각각 달라 저렇게 해놓았다.

 

이제부턴 각 통신을 위한 data class들이다.

data class LoginGoogleRequestModel(
        @SerializedName("grant_type")
        private val grant_type: String,
        @SerializedName("client_id")
        private val client_id: String,
        @SerializedName("client_secret")
        private val client_secret: String,
        @SerializedName("code")
        private val code: String
    )

 

data class LoginGoogleResponseModel(
    @SerializedName("access_token") var access_token: String,
)

 

data class SendAccessTokenModel(
    private val accessToken:String
)

 

https://github.com/inu-appcenter/inu-events-android/tree/heejik/app/src/main/java/org/inu/events/login

 

GitHub - inu-appcenter/inu-events-android: INU 행사 알림/신청 플랫폼 안드로이드입니다.

INU 행사 알림/신청 플랫폼 안드로이드입니다. Contribute to inu-appcenter/inu-events-android development by creating an account on GitHub.

github.com

 

 

참고:

https://developers.google.com/identity/sign-in/android/start-integratinghttps://stackoverflow.com/questions/33998335/how-to-get-access-token-after-user-is-signed-in-from-gmail-in-android

https://velog.io/@mraz3068/Android-Google-Login-Access-%ED%86%A0%ED%81%B0-%EC%96%BB%EB%8A%94-%EB%B0%A9%EB%B2%95%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90

https://modelmaker.tistory.com/entry/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-Debug-SHA-Key-%EC%B6%94%EC%B6%9C-%EB%B0%A9%EB%B2%95