Grusie 안드로이드 개발 기술 블로그

[Android] 파이어베이스 이메일 회원가입 - 2 (이메일 인증, 딥링크 회고) 본문

안드로이드 개발/파이어베이스

[Android] 파이어베이스 이메일 회원가입 - 2 (이메일 인증, 딥링크 회고)

grusie 2024. 3. 15. 22:14
728x90
반응형
SMALL

이전에 잘못 된 판단으로 인한 플로우 변경을 시도한다.

이메일 인증을 지원하는 것 같기에 그걸 사용해 보려고 한다. 이메일 인증을 요청하기 이전 내용들을 동일하기에 이전 글과 이어서 작성한다. 파이어베이스 설정, 동적링크생성, 도메인 적용 등은 이전 글을 참고하도록 하자.

2024.03.15 - [안드로이드 개발] - [Android] 파이어베이스 이메일 회원가입 - 1 (이메일 링크 인증)

 

[Android] 파이어베이스 이메일 회원가입 - 1 (이메일 링크 인증)

파이어베이스 설정을 완료 하였으니, 이제 회원가입과 로그인을 다뤄야 할 차례이다. 제일 먼저 이메일로 인증을 하는 방법을 먼저 알아보자. 만약 파이어베이스 설정을 아직 완료하지 않았다

grusie.tistory.com

 

sendEmailVerification()을 활용하여 인증 메일을 보낼 수 있다.

 

하지만 현재 로그인 되어있는 사용자에게 인증 메일을 보내는 것이기에, 이 전에 말했던 플로우로 진행을 할 것이다.

 

  1. 이메일 인증 버튼을 클릭시 난수로 생성된 비밀번호로 회원가입을 진행(자동 로그인)

  2. 이메일 인증이 완료되었을 시, 기존의 회원을 삭제

  3. 사용자가 입력한 이메일과 비밀번호를 가지고 회원가입 다시 진행

 

우선 난수 암호화 비밀번호를 만드는 로직부터 구현해보자

 

난수 암호화 비밀번호 생성

//랜덤 비밀번호 생성
fun generateRandomBytes(length: Int): ByteArray {
    val random = SecureRandom()
    val bytes = ByteArray(length)
    random.nextBytes(bytes)
    return bytes
}

//랜덤 비밀번호 암호화
fun encryptData(data: ByteArray, key: String): String {
    val byteKey = stringToByteArray(key)
    val cipher = Cipher.getInstance("AES")
    val secretKey = SecretKeySpec(byteKey, "AES")
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)
    return cipher.doFinal(data).joinToString(",")
}

//랜덤 비밀번호 키 생성(최초 1회만 진행)
fun generateAESKey(): ByteArray {
    val keyGenerator = KeyGenerator.getInstance("AES")
    keyGenerator.init(256)
    val secretKey = keyGenerator.generateKey()
    return secretKey.encoded
}

generateAESKey()를 사용하여 난수 비밀번호 키를 만들고, 이것을 Local.Properties에 저장하고 BuildConfig에서 불러와서 외부에 노출되지 않도록 처리한다.

// 난수 비밀번호 생성
val randomData =
    Utils.generateRandomBytes(16)

// AES 키
val aesKey = BuildConfig.PASSWORD_AES_KEY

// 데이터 암호화
val encryptedPassword = Utils.encryptData(randomData, aesKey)

인증버튼을 눌렀을 때, 동작하는 코드들로, 난수 비밀번호를 생성하여 암호화하여, 파이어베이스에 임시 회원가입 및 로그인을 진행한다.

 

UseCase 전체 코드

    suspend operator fun invoke(email: String): FirebaseUser {
        try {
            // 난수 비밀번호 생성
            val randomData =
                Utils.generateRandomBytes(16)
            // AES 키
            val aesKey = BuildConfig.PASSWORD_AES_KEY
            // 데이터 암호화
            val encryptedPassword = Utils.encryptData(randomData, aesKey)


            auth.createUserWithEmailAndPassword(email, encryptedPassword).await()
            val user = auth.currentUser!!

            val url = BuildConfig.FIREBASE_DYNAMIC_LINK + Constant.VERIFY_PREFIX + "?uid=" + user.uid

            val dynamicLink =
                FirebaseDynamicLinks.getInstance().createDynamicLink()
                    .setLink(Uri.parse(url))
                    .setDomainUriPrefix(BuildConfig.FIREBASE_DYNAMIC_LINK)
                    .setAndroidParameters(
                        DynamicLink.AndroidParameters.Builder().build()
                    )
                    .buildShortDynamicLink().await()

            val actionCodeSettings = ActionCodeSettings.newBuilder()
                .setUrl(dynamicLink.shortLink.toString())
                .setAndroidPackageName("com.grusie.policyInfo", true, null)
                .setHandleCodeInApp(true)
                .build()

            user.sendEmailVerification(actionCodeSettings).await()

            return user

        } catch (e: Exception) {
            auth.currentUser?.delete()
            throw e
        }
    }

 

혹시나 임시 회원가입을 했는데, 이메일 인증을 진행하던 중에, 에러가 날 경우를 대비해서 catch에서 currentUser가 있다면, delete()를 실행시킨다.

 

정상적으로 메일이 오는 것을 볼 수 있다.

메일의 형태는 파이어베이스 콘솔에서 변경 가능하다.

 

해당 링크를 클릭하면, 승인이 완료 되었다고 나오며, Continue를 누를 경우 Dynamic Link로 이동된다.

Dynamic Link처리

의존성 추가

dependencies {
    // Import the BoM for the Firebase platform
    implementation(platform("com.google.firebase:firebase-bom:32.3.1"))

    // Add the dependencies for the Dynamic Links and Analytics libraries
    // When using the BoM, you don't specify versions in Firebase library dependencies
    implementation 'com.google.firebase:firebase-dynamic-links-ktx'
    implementation 'com.google.firebase:firebase-analytics-ktx'
}

모듈(앱 수준) Gradle 파일에 dynamic Link에 대한 의존성 추가

 

딥 링크에 대한 인텐트 필터 추가

<activity
    android:name=".MainActivity"
    android:exported="true"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>

    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data
            android:host="policyinfo.page.link"
            android:pathPrefix="/verify"
            android:scheme="https" />

    </intent-filter>
</activity>

다이나믹 링크를 타고 앱으로 들어오기 위해서는 매니페스트를 수정해주어야 한다. autoVerify를 true로 해주고, 카테고리들을 추가한 뒤 데이터를 삽입한다.

data 속성들은 다음과 같다.

host : 다이나믹 링크의 도메인에 해당한다.

pathPrefix : 실제 사용할 도메인 하위를 나타낸다. (선택)

scheme : 다이나믹 링크의 스키마

 

참고) 동적링크를 타고 들어왔을 때, 앱이 열려 있는 상태일 때 새로 그려지면 데이터들이 날아가기 때문에, LauchMode를 SingleTask로 변경하여 데이터들을 유지 할 수 있도록 구현하였다.

딥 링크 처리

val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(true) {
    lifecycleOwner.lifecycleScope.launch {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
            try {
                val pendingDynamicLinkData =
                    Firebase.dynamicLinks.getDynamicLink((context as Activity).intent)
                        .await()
                val deepLink = pendingDynamicLinkData?.link

                if (deepLink != null) {
                    Log.d("confirm deepLink : ", "$deepLink")
                    val uid = deepLink.getQueryParameter(Constant.PARAM_UID)

                    uid?.let { viewModel.isVerified(it) }
                }
            } catch (e: Exception) {
                viewModel.setSignUpUiState(SignUpUiState.Error(e))
            }
        }
    }
}

Compose에서의 딥 링크를 처리하는 방법에 대해 고민을 하던 중 LauchedEffect를 사용하면 되겠지 하고 막연히 생각하였다.

하지만 딥링크를 클릭해서 넘어오더라도, LauchMode가 SingleTask이기 때문에 화면을 새로 그리지 않아, 동작하지 않았고, 찾아보던 중 라이프사이클을 감지할 수 있는, LifecycleOwnerScope가 있었기에 그것을 사용하여, onResume일 때를 감지해, 딥 링크의 존재 유무를 판단하도록 구현하였다.

 

딥 링크가 존재 할 경우 뷰모델에서 처리를 하도록 하였다.

딥링크 처리

UI에 연결하면 이쁘게 작동하는 것을 볼 수 있다.

 

생각해보니, 딥링크를 통해 들어오지 않을 수도 있고, 그럴 경우에도 이메일이 인증 되어 있는지 판단해야했다.

어차피 RESUME 될 때마다, 인증 되었는지 판단할 것이라면.... 딥링크를 굳이 만들 필요가 없어졌다. uid로 구분하는 게 아니기 때문이다... 고생한 게 너무 아깝지만 딥 링크 부분을 지우고 해야겠다.

 

어차피 DynamicLink가 Deprecated 된다고 하니 없애버리는 게 차라리 잘 된 것 일지도 모르겠다.

 

SignUpScreen()

val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(true) {
    lifecycleOwner.lifecycleScope.launch {
        lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
            viewModel.isVerified()
        }
    }
}

SignUpViewModel()

    suspend fun isVerified() {
        if (useCases.emailVerifyUseCase()){
            changeVerifyChecked(true)
        }
        return
    }

EmailVerifyUseCase()

class EmailVerifyUseCase(private val auth : FirebaseAuth) {
    suspend operator fun invoke(): Boolean {
        auth.currentUser?.let {
            it.reload().await()
            if (it.isEmailVerified) {
                return true
            }
        }
        return false
    }
}

EmailSendUseCase()

suspend operator fun invoke(email: String): FirebaseUser {
    try {
        // 난수 비밀번호 생성
        val randomData =
            Utils.generateRandomBytes(16)
        // AES 키
        val aesKey = BuildConfig.PASSWORD_AES_KEY
        // 데이터 암호화
        val encryptedPassword = Utils.encryptData(randomData, aesKey)

        auth.createUserWithEmailAndPassword(email, encryptedPassword).await()
        val user = auth.currentUser!!

        user.sendEmailVerification().await()

        return user

    } catch (e: Exception) {
        auth.currentUser?.delete()
        throw e
    }
}

한층 조촐해진 코드이다.

 

참고

https://firebase.google.com/docs/auth/android/manage-users?hl=ko&_gl=1*1uj6sy9*_up*MQ..*_ga*MTExMTI3OTU2OC4xNzEwNDkyOTUw*_ga_CW55HF8NVT*MTcxMDQ5Mjk1MC4xLjAuMTcxMDQ5Mjk1MC4wLjAuMA..

 

Firebase에서 사용자 관리하기  |  Firebase Authentication

Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 Firebase에서 사용자 관리하기 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요

firebase.google.com

 

https://firebase.google.com/docs/dynamic-links/android/receive?hl=ko&_gl=1*aygsdk*_up*MQ..*_ga*MTg1MzQ0MDI2OS4xNzEwNDgwMDMz*_ga_CW55HF8NVT*MTcxMDQ4MDAzMy4xLjAuMTcxMDQ4MDAzMy4wLjAuMA..

 

Android에서 Firebase 동적 링크 수신  |  Firebase Dynamic Links

Google I/O 2023에서 Firebase의 주요 소식을 확인하세요. 자세히 알아보기 의견 보내기 Android에서 Firebase 동적 링크 수신 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하

firebase.google.com

 

코드

https://github.com/Grusie/PolicyInfoApp

 

GitHub - Grusie/PolicyInfoApp: Clean Architecture + MVVM + Hilt + Compose를 적용한 청년정책 앱

Clean Architecture + MVVM + Hilt + Compose를 적용한 청년정책 앱 - Grusie/PolicyInfoApp

github.com

 

후기

딥링크 처리를 하는 부분에서 많은 고전을 했지만, 그 이후로는 완전한 이해를 바탕으로 개발을 진행했기에 금방금방 개발을 했던 것 같다.

이번에 공부했던 이론을 가지고 앞으로도 금방 회원가입을 구현 해 낼 수 있을 것 같다.

다음 시간에는 인증 받은 이메일과 사용자의 비밀번호를 가지고, 제대로된 회원가입을 진행해 볼 예정이다.

반응형
LIST