OAuth 기반 로그인을 구현하기위해 먼저 카톡부터해보자.
https://developers.kakao.com/
사이트에 들어가서 상단의 내 애플리케이션
을 누른다.
애플리케이션 생성을 누르고 정보를 입력한다.
플랫폼 설정에 들어가서 패키지명과 키 해시를 입력한다.
키 해시는 터미널 명령어를 통해 얻을 수 있다.
Windows(openSSL을 설치해야한다)
keytool -exportcert -alias androiddebugkey -keystore %USERPROFILE%\.android\debug.keystore -storepass android -keypass android | openssl sha1 -binary | openssl base64
Mac
keytool -exportcert -alias androiddebugkey -keystore ~/.android/debug.keystore -storepass android -keypass android | openssl sha1 -binary | openssl base64
만약 본인이 배포전 앱이 아닌 실제 배포중인 앱이 적용하고 싶다면?
릴리즈 키 해시를 발급받아 넣는다.
Windows:keytool -exportcert -alias <RELEASE_KEY_ALIAS> -keystore <RELEASE_KEY_PATH> | openssl sha1 -binary | PATH_TO_OPENSSL_LIBRARY\bin\openssl base64
Mac:keytool -exportcert -alias <RELEASE_KEY_ALIAS> -keystore <RELEASE_KEY_PATH> | openssl sha1 -binary | openssl base64
그리고 카카오 로그인에 들어가 활성화 설정 상태를 ON 한다.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.sample">
<!-- 인터넷 사용 권한 설정-->
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
인터넷을 쓰기에 Manifest에서 인터넷 사용을 허용한다.
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
Kakao Login SDK가 Java 1.8을 사용하기때문에 자바버전을 명시하기 위해 build.gralde(Module)을 위처럼 바꿔준다.
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url = java.net.URI("https://devrepo.kakao.com/nexus/content/groups/public/") }
}
}
카카오 SDK를 위해 settings.gradle(Project)
를 다음처럼 설정한다.
dependencies {
implementation "com.kakao.sdk:v2-all:2.20.1" // 전체 모듈 설치, 2.11.0 버전부터 지원
implementation "com.kakao.sdk:v2-user:2.20.1" // 카카오 로그인 API 모듈
implementation "com.kakao.sdk:v2-share:2.20.1" // 카카오톡 공유 API 모듈
implementation "com.kakao.sdk:v2-talk:2.20.1" // 카카오톡 채널, 카카오톡 소셜, 카카오톡 메시지 API 모듈
implementation "com.kakao.sdk:v2-friend:2.20.1" // 피커 API 모듈
implementation "com.kakao.sdk:v2-navi:2.20.1" // 카카오내비 API 모듈
implementation "com.kakao.sdk:v2-cert:2.20.1" // 카카오톡 인증 서비스 API 모듈
}
SDK 설치를 위해 build.gradle(Project)
에 원하는 SDK를 Implementation한다.
여기서는 implementation "com.kakao.sdk:v2-user:2.12.1"
를 사용한다.
구글맵, 카카오 로그인 등 어떠한 API를 사용하면 Key를 기반으로 권한을 확인하고 사용한다.
이런 API Key는 노출되면 타인이 맘대로 사용할 수도 있어 위험하다.
Github에 API Key를 노출시킨 채 푸시하게 되면, Security Alert가 오지게 뜬다..
이러지 않기위해 Android에서는 다양한 방법으로 키를 숨기는데 그 중 한가지 방법이 있다.
API Key들을 local.properties(SDK Location)
에 저장한다.
그리고 .gitignore
에 local.properties
를 넣는다.
보통은 프로젝트를 생성하면 자동으로 안드로이드 스튜디오에서 만들어준다.
Kakao API Key는 아까 카카오 디벨로퍼에서 생성한 애플리케이션 메인화면에서 볼 수 있다.
네이티브 앱 키가 Kakao API Key이며, kakao + 네이티브 앱 키 = Kakao Redirect Uri이다.
이 사진으로 예를 들면 kakao api key = 04f~~~, kakao redirect uri = kakao04f~~~ 이 되겠다.
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
이제 다시 build.gradle(Module)
로 와서 local.properties
를 인식할 수 있도록 다음과 같은 코드를 쓴다.
gradle
이라는 언어의 문법인데, 대충 파일을 불러와 key, value로 읽을 수 있는 코드라 보면 된다.
android {
namespace 패키지명
compileSdk 33
defaultConfig {
applicationId 패키지명
minSdk 24
targetSdk 33
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
buildConfigField "String", "KAKAO_API_KEY", properties["KAKAO_API_KEY"]
resValue "string", "KAKAO_REDIRECT_URI", properties["KAKAO_REDIRECT_URI"]
}
~~~
그리고 android.defaultConfig 안에 다음과 같이 키를 넣는다. 파일은 그대로 build.gradle(Module)
이다.
<activity
android:name="com.kakao.sdk.auth.AuthCodeHandlerActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="oauth"
android:scheme="@string/KAKAO_REDIRECT_URI" />
</intent-filter>
</activity>
카카오톡 로그인을 할때, 카카오톡을 열거나, 웹브라우저에서 카카오톡 로그인 창을 띄워야하므로 intent-filter가 필요하다.
그래서 AndroidManifest.xml
에 다시 들어가 <Application>
태그 내부에 다음의 코드를 작성한다.
build.gradle
에서 resValue
명령어를 통해 local.properties
에 있는 값을 res/string
으로 취급할 수 있도록 했기에, @string/KAKAO_REDIRECT_URI
로 접근할 수 있는 것이다.
Android에서는 Activity와 Application이라는 용어가 있다.
간단히 말하자면, Activity는 페이지 한개를 의미하고 Application은 앱하나를 의미한다.
Jetpack Compose는 Activity하나에서 NavHost를 통해 Sub Composable를 보여줬다 숨기다를 반복하므로, 사실 React의 SPA와 비슷한 원리이다.
즉, Jetpack Compose는 MainActivity하나만 필요하다고 해도 무방하다.
그러나, SDK의 초기화 설정 같은 것은 Application에서 선언한다.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
RetrofitInstance.init(this)
KakaoSdk.init(this, BuildConfig.KAKAO_API_KEY)
}
}
다음과 같이 Application을 선언하고 Kakao SDK를 초기화 한다.
아까 build.gradle
에서 buildConfileField로 값을 넘겼기에 Application에서 이를 받아 초기화가 가능하다.
AndroidManifest.xml
에서 <application>
태그 속성에 name으로 방금 생성한 클래스의 이름을 넣는다.
fun createKakaoToken() {
// 로그인 조합 예제
// 카카오계정으로 로그인 공통 callback 구성
// 카카오톡으로 로그인 할 수 없어 카카오계정으로 로그인할 경우 사용됨
val callback: (OAuthToken?, Throwable?) -> Unit = { token, error ->
if (error != null) {
_loginApiState.value = ApiState.Error("카카오계정으로 로그인 실패")
} else if (token != null) {
로그인성공에대한로직()
}
}
// 카카오톡이 설치되어 있으면 카카오톡으로 로그인, 아니면 카카오계정으로 로그인
if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
UserApiClient.instance.loginWithKakaoTalk(context = context) { token, error ->
if (error != null) {
// 사용자가 카카오톡 설치 후 디바이스 권한 요청 화면에서 로그인을 취소한 경우,
// 의도적인 로그인 취소로 보고 카카오계정으로 로그인 시도 없이 로그인 취소로 처리 (예: 뒤로 가기)
if (error is ClientError && error.reason == ClientErrorCause.Cancelled) {
return@loginWithKakaoTalk
}
// 카카오톡에 연결된 카카오계정이 없는 경우, 카카오계정으로 로그인 시도
UserApiClient.instance.loginWithKakaoAccount(context, callback = callback)
} else if (token != null) {
로그인성공에대한로직()
}
}
} else {
UserApiClient.instance.loginWithKakaoAccount(context = context, callback = callback)
}
}
위 코드를 사용할 수 있다.
로그인 성공에 대한 로직으로 SharedPreferences에 카카오 개인 ID를 저장할 수도 있고, 저장한 ID를 바탕으로 서버에서 JWT 토큰을 발급받도록 할 수도 있고 프로젝트의 기능 정책에 따라 입맛대로 구현하면 된다.
UserApiClient.instance.logout { error ->
if (error != null) {
Log.e(TAG, "로그아웃 실패. SDK에서 토큰 삭제됨", error)
}
else {
Log.i(TAG, "로그아웃 성공. SDK에서 토큰 삭제됨")
}
}
여담으로 로그아웃 코드는 다음과 같다.
구글도 카카오랑 유사하다. 둘다 OAuth 기반이고, 워낙 대기업이기에 방식이 유사하다.
https://console.cloud.google.com/
에 들어가 프로젝트를 만들고 API 및 서비스 -> 사용자 인증 정보에 들어간다.
아까 카카오톡에서 했던것과 똑같은 정보를 입력하면 된다.
카카오톡에서는 키 해시를 사용했지만, 구글은 SHA-1을 입력해야한다.
OAuth 2.0 클라이언트가 생성되었을텐데, 들어가서 API Key를 확인한다.
이것도 local.properties
에 넣고 build.gradle(Module)
에서 지지고 볶고~~ 그 긴거를 해서 Google SDK를 초기화할때 쓸거니까 말이다.
OAuth 동의 화면에 들어가 주어진 대로 값을 채워준다.
대신, 본인 앱이 아직 배포전 디버그 상태라면 테스트 사용자에 로그인이 가능하도록 이메일을 직접 추가해야한다.
테스트 사용자는 최대 100명이다.
하지만 구현 중 Firebase를 사용하지 않은 Google OAuth 기능은 xml 기반의 Intent 로 로그인을 진행하는 레거시 코드라는 문제가 있었다.
그래서 Jetpack Compose로는 Firebase를 사용하지 않고 Google OAuth를 활용하기 어려운 난관이 있었다.
수많은 검색을 통해 의문의 Jetpack Compose 장인이 올린 영상을 참고해 따라할 수 있었다.
class GoogleApiContract : ActivityResultContract<Int, Task<GoogleSignInAccount>?>() {
override fun createIntent(context: Context, input: Int): Intent {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(BuildConfig.GOOGLE_OAUTH_CLIENT_ID)
.requestId()
.build()
val intent = GoogleSignIn.getClient(context, gso)
return intent.signInIntent
}
override fun parseResult(resultCode: Int, intent: Intent?): Task<GoogleSignInAccount>? {
return when (resultCode) {
Activity.RESULT_OK -> {
GoogleSignIn.getSignedInAccountFromIntent(intent)
}
else -> null
}
}
}
ActivityResultContract를 작성한다.
이는 Jetpack Compose에서 Intent를 실행할 수 있도록 도와주는 역할을 한다.
이것을 View에서 사용한다.
@Composable
fun LoginScreen() {
val authResultLauncher = rememberLauncherForActivityResult(
contract = GoogleApiContract()
) { task ->
viewModel.handleGoogleSignInResult(task)
}
// UI들~~~
// UI 중 버튼의 clickable에 authResultLauncher.launch(1) 을 넣는다.
}
authResultLauncher.launch(1)
을 실행하면, GoogleApiContract의 task가 실행된다.
// ViewModel 내부
fun handleGoogleSignInResult(task: Task<GoogleSignInAccount>?) {
_loginApiState.value = ApiState.Loading
if (task == null) {
_loginApiState.value = ApiState.Error("Google 로그인 실패")
return
}
try {
val account = task.getResult(ApiException::class.java)
account?.let {
Log.d("LoginViewModel", "Google sign in success: ${account.id}")
_loginApiState.value = ApiState.Success("Google 로그인 성공")
} ?: run {
_loginApiState.value = ApiState.Error("Google 로그인 실패")
}
} catch (e: ApiException) {
Log.e("LoginViewModel", "Google sign in failed: ${e.statusCode}")
_loginApiState.value = ApiState.Error("Google 로그인 실패")
}
}
task는 구글 로그인 화면을 여는 Intent를 실행하는 것이며, 성공적으로 뷰모델 내부에서 값을 받아 GoogleSignInAccount
라는 태스크의 결과를 받을 수 있다.