Skip to content

OAuth 2.0 API 구글 로그인 in 안드로이드 document

SeungGun edited this page Jul 1, 2022 · 2 revisions

안드로이드에서의 OAuth 2.0 구글 로그인 사용법 Documentation

  • written by SeungGun
  • kotlin 사용

Google Cloud Platform 설정

  • 참고 사이트

    https://rkdxowhd98.tistory.com/168

  • API 및 서비스의 사용자 인증 정보에서 사용자 인증 정보 만들기 하여 OAuth 2.0 클라이언트 ID 생성

    • 웹 애플리케이션 유형의 클라이언트 ID가 백엔드 서버의 OAuth 2.0 클라이언트 ID임.

      → 클라이언트 ID와 보안 비밀번호 정보 필요

안드로이드 스튜디오 프로젝트 설정

build.gradle 파일에서 Google Play 서비스 dependency 추가

dependencies{
    implementation 'com.google.android.gms:play-services-auth:20.2.0'
}

res/values/strings.xml에 클라이언트 ID string 추가

<resources>
	<string name="server_client_id">CLIENT ID FROM API</string>
</resources>

Http 요청을 위한 AndroidManifest.xml 설정

<uses-permission android:name="android.permission.INTERNET"/>
<application
    android:usesCleartextTraffic="true"
    ...
            >
</application>

Kotlin으로 구글 로그인 구현

우리 프로젝트의 로그인 ~ 백엔드 서버 토큰 검증 프로세스

1. 구글 로그인 버튼 클릭 
2. 버튼 클릭시 구글 로그인 요청(id token, authCode를 얻기 위함)
3. 받아온 authCode와 클라이언트 ID와 클라이언트 보안 비밀번호를 가지고 구글 서버로 엑세스 토큰 요청
4. 엑세스 토큰 요청의 응답으로 access token과 id token 값 가져오기
5. 가져온 access token과 id token을 body로 백엔드 서버에 계정 검증 요청
6. header의 JWT 토큰(key = authorization) 유무 판단
   6-1. 토큰이 있으면 회원가입된 유저 → 기존 유저
   6-2. 토큰이 없으면 존재하지 않는 계정 → 회원가입 페이지로 이동

공통 코드 - 구글 로그인 자체

<com.google.android.gms.common.SignInButton
        android:id="@+id/sign_in_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
/* 각 변수를 전역으로 사용하고 onCreate()에서 초기화하기 위해 lateinit으로 정의 */
lateinit var gso: GoogleSignInOptions 
lateinit var client: GoogleSignInClient
const val RC_SIGN_IN = 100
/* ---------------- properties ----------------- */

/* 로그인 할 때 요청 옵션 지정 - Chaning 기법 사용
	→ 인자에 API로부터 받아온 클라이언트 ID 사용(strings.xml에 정의한 string 사용)
*/
gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
	.requestIdToken(getString(R.string.server_client_id)) // 사용자의 id token 
	.requestServerAuthCode(getString(R.string.server_client_id)) // auth code
	.requestEmail() // 사용자 이메일 - 인자 x
	.build()

/* 구글 로그인 클라이언트 초기화 */
client = GoogleSignIn.getClient(this, gso)

/* 로그인 버튼 View 정의 및 버튼 이벤트 정의 */
val signInButton = findViewById<SignInButton>(R.id.sign_in_button)
signInButton.setOnClickListener{
    onLogin()
}

/* ----------------- onCreate() ----------------*/

fun onLogin(){
    /* 구글 로그인 하는 Intent 호출 */
    val signInIntent: Intent = client.signInIntent
    startActivityForResult(signInIntent, RC_SIGN_IN)
}

/* 구글 로그인 Intent로 부터 돌아왔을 때 처리 */
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?){
    super.onActivityResult(requestCode, resultCode, data)
    if(data == null)
    	Log.d("TAG", "data is null")
    when(requestCode){
        /* requestCode가 구글 로그인 Intent 호출에 사용한 requestCode일 때 */
        RC_SIGN_IN -> {
            /* 받아온 intent data로 부터 구글 로그인 결과 가져오기 */
            val task = GoogleSignIn.getSignedInAccountFromIntent(data)
            /* 결과로부터 계정 정보 가져오기(null일 수 있음) */
            val account = task.result
            println("user email", account.email ?: "") // 이메일 정보
            println("user idToken", account.idToken ?: "") // 사용자 id token 값
            println("user photoUrl", account.photoUrl ?: "") // 프로필 이미지 url
            println("user displayName", account.displayName ?: "") // 표기 이름
            println("user familyName", account.familyName ?: "") //
            println("user givenName", account.givenName ?: "") // 이름
            println("Server AuthCode", account.serverAuthCode ?: "") // 인가코드(auth code)
            
            // 구글 및 백엔드 서버 요청하는 다음 코드
        }
    }
}
  • 다음 코드

OkHttp 라이브러리 사용하여 Http 요청(구글 서버와 백엔드 서버 요청)

  • build.gradle에 OkHttp dependency 추가
dependencies{
    ...
    implementation(platform("com.squareup.okhttp3:okhttp-bom:4.10.0"))
    // define any required OkHttp artifacts without version
    implementation("com.squareup.okhttp3:okhttp")
    implementation("com.squareup.okhttp3:logging-interceptor")
    ...
}
/* 메인 쓰레드에서 네트워크 작업을 못하므로 새로 Worker 쓰레드 생성 */
thread(start = true){
    val client = OkHttpClient() // OkHttp 클라이언트 객체 생성
    
    /* 구글 서버에 요청하기 위한 요청 Body */ 
    val requestBody = FormBody.Builder()
    .add("client_id", getString(R.string.server_client_id)) // 클라이언트 ID
    .add("client_secret", "클라이언트 보안 비밀번호") // 클라이언트 보안 비밀번호
    .add("code", account.serverAuthCode) // 로그인 시 받아온 authCode 
    .add("grant_type", "authorization_code") // 이 값으로 고정
    .add("redirect_uri", "") // 리다이렉트 필요 없어서 빈 문자열로 채움
    .build()
    
    /* 구글 서버에 상단 요청 Body와 함께 POST 준비*/
    val request = Request.Builder()
    	.url("https://oauth2.googleapis.com/token")
    	.post(requestBody)
    	.build()
    
    /* Http 요청하기 */ 
    client.newCall(request).enqueue(object: Callback{
        /* 응답 실패했을 때 콜백 */ 
        override fun onFailure(call: Call, e: IOException){
            // 요청 실패
        }
        /* 응답 성공했을 때 콜백*/
        override fun onResponse(call: Call, response: Response){
            val body: String = response.body!!.string() // 응답 Body(json form) 가져오기
            
            val responseJson = JSONObject(body) // json 문자열을 json 객체로 전환 
            val accessToken = responseJson.optString("access_token") // access token 값 가져오기
            val idToken = responseJson.optString("id_token") // id token 가져오기
            
            val newJsonObject = JSONObject() // 새로운 json 객체 생성
            newJsonObject.put("accessToken", accessToken) // json 객체에 accessToken이라는 key로 access token 값 넣기
            newJsonObject.put("idToken", idToken) // json 객체에 idToken이라는 key로 id token 값 넣기
            
            val client2 = OkHttpClient() // 새로운 OkHttp 클라이언트 객체 생성
            
            /* 새로 만든 json 객체를 문자열로 변환하고 Media type을 json으로 지정 한 뒤, 이 문자열을 request body로 전환 */
            var rqBody = newJsonObject.toString().toRequestBody("application/json; charset=utf-8".toMediaType())
            
            /* 백엔드 서버 url에 상단 요청 body와 함께 요청 준비 */
            val req = Request.Builder()
            	.url("Server url")
            	.post(rqBody)
            	.build()
            
            /* Http 요청하기 */
            client2.newCall(req).enqueue(object: Callback{
                /* 응답 실패했을 때 콜백 */
                override fun onFailure(call: Call, e: IOException){
                 	// 요청 실패    
                }
                /* 응답 성공했을 때 콜백 */
                override fun onResponse(call: Call, response: Response){
                    println(response.body?.string())
                    
                    /* 응답 헤더의 authroization의 값이 null인지 아닌지 판단 
                    	null이면 JWT 토큰이 없으므로 회원가입이 필요로 하는 사용자
                    	아니라면 기존 회원가입된 사용자를 의미
                    */
                    println(response.headers['authorization'] ?: "null 입니다.")
                }
            })
        }
    })
}

Retrofit 라이브러리 사용하여 Http 요청

이미 구글 로그인이 되어 있는지 확인하기

  • onStart() 생명주기에서 체크
override fun onStart(){
    super.onStart()
    val account = GoogleSignIn.getLastSignedInAccount(this)
    if(account != null){ // 이미 로그인 되어 있다면
        println("user email", account.email ?: "") // 이메일 정보
        println("user idToken", account.idToken ?: "") // 사용자 id token 값
        println("user photoUrl", account.photoUrl ?: "") // 프로필 이미지 url
        println("user displayName", account.displayName ?: "") // 표기 이름
        println("user familyName", account.familyName ?: "") //
        println("user givenName", account.givenName ?: "") // 이름
        println("Server AuthCode", account.serverAuthCode ?: "") // 인가코드
    }
    else{
        //... 로그인이 안되어 있을 때에 대한 처리
    }
}

구글 로그 아웃 및 계정 연결 해제

/* client는 로그인할 때 생성한 구글 클라이언트 객체*/
client.signOut().addOnCompleteListener(this){
	if(it.isSuccessful){
		println("로그아웃 성공")
	}
}

구글 로그인 구현 시 발생하는 에러

com.google.android.gms.common.api.ApiException: 10

  • 핑거프린트 정보가 일치하지 않을 때 발생하는 exception

    • 구글 클라우드 플랫폼에서 OAuth 클라이언트 ID를 생성할 때 SHA-1 인증서 디지털 지문에 대한 문제로 판단이 됨.

    • 팀원의 SHA-1 값에 대한 안드로이드 유형의 클라이언트 ID도 사용해보고, 필자의 SHA-1 값에 대한 안드로이드 유형의 클라이언트 ID으로 사용해봤지만 동일한 에러 발생

      → 웹 애플리케이션 유형의 클라이언트 ID로 사용하니 해결 됨

Clone this wiki locally