[Background]
프로젝트를 진행하면서 소셜 로그인을 도입할 필요가 있었는데 팀원끼리 같이 모든 기능을 구현하기에는 너무 난이도가 높고 무엇보다 보안이 너무 취약할 것 같다는 생각이 들어서 사람들이 가장 많이 사용하는 Google Login을 도입하기로 했습니다. 하지만 인터넷상에 있는 글들은 대부분 Deprecate 된 코드를 쓰지 않나 아니면 잘못된 정보를 전달하는 것 같았습니다.
특히 대부분 블로그들은 Firebase와 Google OAuth을 혼동하지 않던가 아니면 왜 도입하는지 이유조차 모르는 느낌이 강했습니다. 그래서 잘못 정보글 사이에 치여서 도입하는데만 10시간이 걸렸습니다.
저처럼 시간 낭비하는 분들이 나오지 않도록 이번 글을 쓰게 됐습니다.
이번 글에서는 백엔드 서버가 따로 있다고 가정하고 쓰겠습니다
이 글에 사용된 버전들
- Java 17
- Gradle 8.0.2
- kotlin 1.8.22
- Google Auth 20.7.0
이 글은 2023/09/15에 작성된 글로 나중에는 도입하는 방법이 바뀔 수도 있습니다
[Firebase vs Google OAuth]
구글 로그인을 도입하는 방법은 총 두 가지가 있습니다 Firebase을 통하거나 아니면 직접 Google Cloud ApI Service에다가 OAuth정보를 입력하거나가 있습니다.
Firebase을 사용하면 등록된 정보가 자동으로 Google Cloud API Service OAuth에 등록되고 모든 건 Firebase만으로 로그인을 구현할 수 있는데 반면 google-service.json 같은 파일 추가, dependencies추가 같이 할 일이 더 많아 좀 더 복잡한 느낌이었습니다.
무엇보다 백엔드 서버가 따로 있으면 결국 직접 Google Cloud API Service을 다뤄야 할 때가 많습니다.
Google OAuth 같은 경우에는 자동으로 도입되지 않기 때문에 사전에 알아야 할 것들이 있습니다.
간단하게 정리하는 이와 같습니다.
Firebase | Google Oauth |
+ 자동으로 Google Oauth에 정보를 등록해준다 | + Firebase에 비해서 도입하기 쉽다 |
+ 다른 플렛폼 로그인 도입할 수도 있다 | - 사전에 알고 있어야하는 정보들이 있다 |
- google-service.json 파일 추가하는 등 스탭이 더 많다 | |
- 백엔드 서버가 있다면 결국 Google Oauth을 직접 써야한다 |
이번글에는 Firebase 없이 Google 로그인을 도입하겠습니다.
[우리 서버에 로그인 정보 넘기는 방법]
사용자가 Google 로그인을 하면 우리 서버에 알려줘서 그 사람의 정보를 데이터베이스에서 가져와야 할 때가 많습니다.
그러면 로그인 정보를 서버에 넘겨줘야 하는데, id나 email로 넘겨주기에는 유출될 가능이 너무 높고 보안적으로 취약하다는 점이 있습니다.
이 문제를 해결하기 위해서는 제가 알기로는 크게 두 가지 방법이 있습니다.
1. Redirect URI을 지정하기
Google Cloud API Service에서 등록할 때 이 정보를 입력하는 칸이 있는데 이걸 우리 쪽 서버 엔드 포인트로 지정하게 되면 사용자가 로그인 시 지정한 정보가 구글을 통해 그 엔드포인트로 정보가 전달이 됩니다. 정보 중에 code가 있는데 이걸 이용해서 google access token을 가져올 수 있다.
이 방법은 클라이언트 쪽에서 중요한 정보를 직접 서버에 전달할 필요가 없다는 장점이 있지만, 살짝 웹 쪽에 좀 더 특화된 느낌이 강해서 안드로이드에서 하기에는 조금 무리가 있다.
2. IdToken 넘기기
2번째 방식은 Google에서 사용자 정보를 암호화해서 만든 idtoken을 서버 쪽에 넘기는 방식입니다.
이 방식은 안드로이드용 구글 공식 문서에서 추천하는 방식입니다. 이 방식이 안드로이드한테 가장 어울린 느낌이 들었습니다.
id token을 클라이언트에서 직접 넘겨줘야 하지만 공개키 암호화 방식처럼 private key인 클라이언트 ID를 모르면 복호화를 할 수 없기 때문에 보안에도 뛰어납니다.
이번글에는 이 방식을 사용했습니다.
[직접 도입해 보기]
기본 조건은 이와 같습니다
- Android 5.0 이상을 실행하고 Google Play 스토어 또는 Android 4.2.2 이상을 기반으로 하는 Google API 플랫폼을 실행하고 Google Play 서비스 버전 15.0.0 이상을 실행하는 AVD가 있는 에뮬레이터가 포함된 호환되는 Android 기기.
- SDK 도구 구성요소를 포함한 최신 버전의 Android SDK입니다. SDK는 Android 스튜디오의 Android SDK Manager에서 사용할 수 있습니다.
- Android 5.0(Lollipop) 이상에서 컴파일하도록 구성된 프로젝트입니다.
그리고 settings.gradle이나 최상위 build.gradle에 이개 포함되어 있는지 확인합니다.
google()하고 maven 저장소가 있어야 합니다
allprojects {
repositories {
google()
mavenCentral()
}
}
처음 들어가게 되면 프로젝트를 생성하라고 합니다
프로젝트를 생성해 주시면 됩니다.
프로젝트 생성이 끝났으면 OAuth 동의 화면에 들어가서 외부를 선택하고 무조건 필요한 정보만 입력하시면 됩니다.
설정이 다 끝났으면 테스트 사용자라고 칸이 있는데 여기에 테스트를 진행할 구글 계정을 추가해주셔야 합니다. 이를 통해 원하는 계정만 테스트를 진행할 수 있게 할 수 있습니다
출시를 할 때는 바로 위에 있는 게시 상태에서 앱 게시를 켜면 누구나 로그인을 진행할 수 있습니다.
테스트 사용자에 계정 추가를 안 하면 로그인할 때 이 에러가 뜰 수 있습니다
OAuth 동의 화면 설정이 다 끝나면 사용자 인증 정보에서 OAuth 클라이언트 ID를 추가해 주세요.
총 두 개 클라이언트 ID를 추가해주셔야 합니다.
1. 안드로이드
여기서는 두 가지 정보를 입력해야 합니다, 패키지 이름하고 SHA-1 key
패키지는 app 모듈에 있는 build.gradle에 있는 namespace를 가져오면 되고
android {
namespace = "com.lighthouse.multi_module_navigation"
...
}
SHA-1 key는 터미널에다가./gradlew signingReport 명령어를 치면 나옵니다.
2. 웹
여기서는 따로 입력할 정보는 없고 바로 생성을 해주시면 됩니다.
최종적으로 이렇게 있어야 합니다.
안드로이드와 웹 클라이언트 ID를 만드는 이유는 안드로이드 클라이언트 ID은 지금 프로젝트를 이 Google Cloud API에 연동하는 과정이고
웹은 IdToken이나 AuthCode를 받아오는데 필요한 private key라고 생각하시면 편합니다.
여기까지 하면 설정은 끝났습니다
로그인 관련 코드
가장 처음으로 이 라이브러리를 로그인을 도입할 모듈에 추가해 주세요. 저 같은 경우에는 setting module에 로그인을 도입해서 거기에만 추가했습니다.
implementation("com.google.android.gms:play-services-auth:20.7.0")
로그인의 모든 과정은 자동으로 진행되는데 이 프로세스를 시작할 버튼인 SignInButton을 xml 파일에 추가해 주세요
<com.google.android.gms.common.SignInButton
android:id="@+id/btn_sign_in"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
전체 코드를 보면 이와 같습니다.
class SettingFragment : Fragment() {
private lateinit var binding: FragmentSettingBinding
private val googleSignInClient: GoogleSignInClient by lazy { getGoogleClient() }
private val resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
getResult.value = result.data
}
}
private val getResult = MutableLiveData<Intent?>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?,
): View {
binding = FragmentSettingBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
signInListener()
observeSignInResult()
}
private fun observeSignInResult() {
getResult.observe(viewLifecycleOwner) {
if (it == null) return@observe
val task = GoogleSignIn.getSignedInAccountFromIntent(it)
try {
val account = task.getResult(ApiException::class.java)
val email = account.email ?: ""
getResult.value = null
binding.tvEmail.text = email
Log.d("TESTING", "${account.familyName}, ${account.givenName}, ${account.photoUrl}")
} catch (e: ApiException) {
Log.d("TESTING", e.stackTraceToString())
}
}
}
private fun signInListener() {
binding.btnSignIn.setOnClickListener {
requestGoogleLogin()
}
}
private fun requestGoogleLogin() {
googleSignInClient.signOut()
val signInClient = googleSignInClient.signInIntent
resultLauncher.launch(signInClient)
}
private fun getGoogleClient(): GoogleSignInClient {
val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestScopes(Scope("https://www.googleapis.com/auth/userinfo.email"))
.requestEmail()
.requestId()
.requestProfile()
.requestIdToken(getString(R.string.client_id))
.build()
return GoogleSignIn.getClient(requireActivity(), googleSignInOptions)
}
}
이게 하나씩 살펴보겠습니다.
private val googleSignInClient: GoogleSignInClient by lazy { getGoogleClient() }
private fun getGoogleClient(): GoogleSignInClient {
val googleSignInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestScopes(Scope("https://www.googleapis.com/auth/userinfo.email"))
.requestEmail()
.requestId()
.requestProfile()
.requestIdToken(getString(R.string.client_id))
.build()
return GoogleSignIn.getClient(requireActivity(), googleSignInOptions)
}
googleSignInClient은 어떤 사용자 정보를 받을지 정할 때 사용됩니다. Scope은 Google Cloud API Service에 어떤 사용자 정보를 허용할지 범위를 정해주는 코드입니다, 지금은 userinfo를 허락했습니다.
지금은 email, id, profile 그리고 idtoken을 요청했습니다.
보면 IdToken만 clientid가 필요한데 여기에 웹 클라이언트 id를 추가하면 됩니다.
private val resultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
if (result.resultCode == Activity.RESULT_OK) {
getResult.value = result.data
}
}
private val getResult = MutableLiveData<Intent?>()
private fun observeSignInResult() {
getResult.observe(viewLifecycleOwner) {
if (it == null) return@observe
val task = GoogleSignIn.getSignedInAccountFromIntent(it)
try {
val account = task.getResult(ApiException::class.java)
val email = account.email ?: ""
val idToken = account.idToken ?: ""
getResult.value = null
binding.tvEmail.text = email
// 여기서 서버에 idToken 전달
Log.d("TESTING", "$idToken, ${account.email}, ${account.photoUrl}")
} catch (e: ApiException) {
Log.d("TESTING", e.stackTraceToString())
}
}
}
StartActivityForResult를 만들어주고 저는 liveData을 활용해서 로그인 정보가 성공적으로 내려올 때 업데이트하는 방식을 사용했습니다.
여기서 signInClient에 요청하지 않은 정보는 null로 나오게 되고 아예 로그인이 실패하면 ApiException이 터지게 됩니다.
가장 많이 터지는 에러는 ApiException: 10입니다. 이게 터졌다는 건 client Id idToken 요청할 때 client id가 잘못된 것 일수도 있고 아니면 안드로이드 client id에 패키지 이름과 SHA-1 key가 제대로 등록이 안된 겁니다
com.google.android.gms.common.api.ApiException: 10:
at com.google.android.gms.common.internal.ApiExceptionUtil.fromStatus(com.google.android.gms:play-services-base@@18.0.1:3)
at com.google.android.gms.auth.api.signin.GoogleSignIn.getSignedInAccountFromIntent(com.google.android.gms:play-services-auth@@20.7.0:3)
마지막으로 사용자가 로그인 버튼을 클릭하면 StartActivityForResult에 google client intent을 보내줍니다.
private fun signInListener() {
binding.btnSignIn.setOnClickListener {
requestGoogleLogin()
}
}
private fun requestGoogleLogin() {
googleSignInClient.signOut()
val signInClient = googleSignInClient.signInIntent
resultLauncher.launch(signInClient)
}
모든 게 제대로 도입이 되었다면 요청한 사용자 정보를 받아온 걸 확인할 수 있습니다!
마지막으로 백엔드 사람들한테 private key인 웹 클라이언트 ID을 알려주고 idToken을 서버 통신으로 넘겨줘서 알아서 복호화한뒤 안에 있는 사용자 정보 바탕으로 데이터베이스 업데이트를 하라고 하면 됩니다.
[마무리]
한번 Firebase 내용 없이 Google Cloud API Service만으로 로그인을 도입해 봤습니다. 진짜 알고 보면 엄청 쉬운데 어디에도 안드로이드 클라이언트 ID 말고 웹 클라이언트 ID를 넣어야 한다는 얘기가 없어서 오래 헤맸네요 ㅠ
참고자료
https://developers.google.com/identity/sign-in/android/start-integrating?hl=en
전체코드
https://github.com/flash159483/multi-module-navigation
'android' 카테고리의 다른 글
(android/kotlin) Encrypted Shared Preference 적용 + AEADBadTagException 문제 해결 (0) | 2023.11.17 |
---|---|
(android/kotlin) 멀티모듈에서 proguard 설정하는 방법 (0) | 2023.10.26 |
(android/kotlin) RecyclerView + Admob 적용하기 (0) | 2023.10.13 |