google login api(oauth2) 에 대한 고찰
진행 중인 프로젝트에서 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
이 곳을 들어가보면 나와있는 로직은
1. 구글에 로그인(GCP에 프로젝트 등록 후 구현 가능)
2. 구글에서 id_token값을 발급
3. id_token값을 서버로 보내서 서버에서 작업
이지만 서버에서 access_token값을 원했기에 그냥 내가 구현해서 전달하기로 했다.
사실 위 google 문서에선 예제 코드들이 모두 java로 되어있어 직접 kotlin으로 변환해야 한다.
가장 많은 시간을 소요한 것은 access_token 발급이다.
위 질의응답이 해답이지만 나는 제대로 해석하지 않았고 auth_code값을 다른 방식으로 얻으려고 노력했던 시간이 크다..
사실 간단한 과정이였다. id_token값을 가져오는 것 처럼 그냥 auth_code를 가져오면 됐다.
전체적인 구현 로직을 설명하면
1. 안드로이드에서 구글 로그인을 위해 GCP 에 project가 필요하다
2. 안드로이드 사용자 인증 정보(OAuth 2.0 클라이언트 ID)가 필요하다. 물론 입력에 package명과 SHA-1 서명 인증서 지문이 필요하다.
위와 같이 생성.
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://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