안드로이드 스튜디오 SMS 자동 전송 - andeuloideu seutyudio SMS jadong jeonsong

2018년 10월에 구글에서 공지한 '통화 기록 및 SMS 사용 권한 제한'에 따라

2019년 1월부터 '통화 기록 및 SMS 사용 권한 제한' 정책은 전화나 문자를 주 기능으로 하는 앱이 아니면

SMS 권한을 획득할 수 없다고 합니다.

이 경우 단순히 '인증번호 자동 입력' 기능 구현을 위한 앱은

SMS Retriever API 를 사용하는 방식으로 구현하여야 합니다.

참고

소개

google developer 에서 소개하는 SMS Retriever API 내용을 간략하게 보자면 아래와 같이 설명하고 있습니다.

SMS Retriever API 를 사용하면 사용자가 수동으로 인증 코드를

입력하지 않고도 Android 앱에서 SMS 기반 사용자 확인을 자동으로 수행할 수 있습니다.

SMS Retriever API 는 Play Service 버전 10.2 이상이 설치된 Android 기기에서만 사용 할 수 있습니다

동작

앱에서 자동 SMS 수신을 구현 한다면

과정은 다음과 같습니다.

안드로이드 스튜디오 SMS 자동 전송 - andeuloideu seutyudio SMS jadong jeonsong

  1. 사용자가 앱에서 SMS 인증 단계를 시작하면 전화번호를 입력하라고 요구하거나, Smart Lock for Passwords hint selector 를 이용해서 전화번호를 입력하게 할 수 있습니다.
  2. 앱에서 백엔드 서버에 인증번호를 요청하게 되고, 사용자 데이터베이스에서 사용 가능한 정보에 따라 이 요청에는 사용자의 ID, 사용자의 전화번호 또는 둘 다 포함 될 수 있습니다.
  3. 동시에 앱은 SMS Retriever API 를 호출하여 서버에서 SMS 응답 수신을 시작합니다.
  4. 서버는 서버로 다시 전송할 일회성 코드와 앱을 식별하는 해시를 포함하는 SMS 메시지를 사용자에게 보냅니다
  5. 사용자의 기기가 SMS 메시지를 수신하면 Google Play 서비스는 앱 해시를 사용하여 메시지가 앱용임을 확인하고 SMS Retriever API를 통해 앱에서 메시지 텍스트를 사용할 수 있도록합니다.
  6. 앱은 메시지 텍스트에서 일회용 코드를 구문 분석하여 서버로 다시 보냅니다.
  7. 서버는 앱에서 일회용 코드를 수신하고 코드를 확인한 다음 사용자가 자신의 계정을 성공적으로 확인했음을 기록합니다.

복잡해 보이지만 Hint Selector 기능을 사용하지 않으면 간단하게 구현할 수 있습니다.

안드로이드에서는 Retriever API 를 실행 후

Retriever API 에서 제공하는 Action 이벤트를 필터링 하는 Receiver 를 등록합니다.

디바이스에 SMS 가 수신 될 때

우리가 등록한 Receiver 에게 Retriever API 가 SMS Message 를 알려줍니다.

우리는 전달받은 Text(SMS Message) 를 파싱하여 인증번호를 가져오면 됩니다!

간단하죠?!

의존성 추가

app build.gradle 에 아래 의존성을 추가 합니다.

implementation 'com.google.android.gms:play-services-auth:17.0.0'
implementation 'com.google.android.gms:play-services-auth-api-phone:17.4.0'

기능 구현

먼저 적절한 플로우 시점에 Retriever API 를 starting 시켜줍니다.

저는 send 버튼을 눌렀을 때 starting 시켜주는 걸로 했습니다.

    fun startSmsRetriver() {
        val task = SmsRetriever.getClient(this)
            .startSmsRetriever()

        task.addOnSuccessListener {
            Log.d(LOG, "addOnSuccessListener ${getAppSignatures(this)}")
        }

        task.addOnFailureListener {
            Log.e(LOG, "addOnFailureListener $it")
        }
    }

Receiver Class 를 정의하고 등록하여 줍니다.

    inner class MySMSReceiver : BroadcastReceiver() {

        override fun onReceive(context: Context, intent: Intent) {
            if(SmsRetriever.SMS_RETRIEVED_ACTION == intent.action){
                val extras = intent.extras
                val status = extras?.get(SmsRetriever.EXTRA_STATUS) as? Status

                when(status?.statusCode){
                    CommonStatusCodes.SUCCESS -> {
                        val message = extras?.get(SmsRetriever.EXTRA_SMS_MESSAGE) as? String
                        Log.d(LOG, "onReceive\$SUCCESS $message")

                        if(!message.isNullOrEmpty()){
                            showMessage(message)
                        }
                    }

                    CommonStatusCodes.TIMEOUT -> {
                        Log.d(LOG, "onReceive\$TIMEOUT")
                    }
                }
            }
        }

        fun doFilter(): IntentFilter = IntentFilter().apply {
            addAction(SmsRetriever.SMS_RETRIEVED_ACTION)
        }
    }

    //receiver 등록
    override fun onCreate(savedInstanceState: Bundle?) {
        ....
        registerReceiver(smsReceiver, smsReceiver.doFilter())
    }

    //receiver 해제
    override fun onDestroy() {
         unregisterReceiver(smsReceiver)
         super.onDestroy()
    }

AndroidManifest.xml 에 Receiver 를 등록하여 줍니다.

        
            
                
            
        

그리고 마지막으로 서버로 전송할 Hash Key를 구하여줍니다.

Hash Key 를 구하는 이유는 해당 앱에 해당하는 Hash Key 를 확인하여

Retriever API 가 우리 앱 Receiver 에 이벤트를 날려주기 때문입니다.

Hash Key 가 다르면 이벤트(SMS Message)를 받을 수 없습니다~!

    fun getAppSignatures(context: Context): List {
        val appCodes = mutableListOf()

        try {
            val packageName = context.packageName
            val packageManager = context.packageManager
            val signatures = packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures

            for (signature in signatures) {
                val hash = getHash(packageName, signature.toCharsString())

                if (hash != null) {
                    appCodes.add(String.format("%s", hash))
                }

                Log.d(LOG, String.format("이 값을 SMS 뒤에 써서 보내주면 됩니다 : %s", hash))
            }
        } catch (e: PackageManager.NameNotFoundException) {
            Log.d(LOG, "Unable to find package to obtain hash. : $e")
        }

        return appCodes
    }

    private fun getHash(packageName: String, signature: String): String? {
        val appInfo = "$packageName $signature"

        try {
            val messageDigest = MessageDigest.getInstance(HASH_TYPE)

            // minSdkVersion이 19이상이면 체크 안해도 됨
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8))
            }

            val hashSignature = Arrays.copyOfRange(messageDigest.digest(), 0, NUM_HASHED_BYTES)
            val base64Hash = Base64
                .encodeToString(hashSignature, Base64.NO_PADDING or Base64.NO_WRAP)
                .substring(0, NUM_BASE64_CHAR)

            Log.d(LOG, String.format("\nPackage : %s\nHash : %s", packageName, base64Hash))

            return base64Hash

        } catch (e: NoSuchAlgorithmException) {
            Log.d(LOG, "hash:NoSuchAlgorithm : $e")
        }

        return null
    }

이렇게 되면 Android Client 에서 할 작업은 끝나게 됩니다

이제 서버 쪽에서 해야 할 작업을 보겠습니다.

서버 작업은 사실 크게 할 게 없는데요

이미 SMS 를 보내는 API 가 있다면 

변경이 필요한 부분은 기존의 인증번호를 보내주던 SMS 내용의 변경입니다.

단, 메시지를 Android Client가 수신하기 위해서는 몇 가지 제약조건이 있습니다.

아래 제약 조건만 지켜 SMS를 보내면 쉽게 처리할 수 있습니다.

  1. 메시지 내용은 무조건 <#> 라는 Text로 시작해야한다.
  2. 메시지 내용은 140 byte 이하 여야 한다.
  3. 앱을 식별하는 11자리 Hash Key 로 끝나야 한다. (앱에서 전달 받아야합니다.)

예를 보겠습니다.

<#> 인증번호[1234] OOOO 에서 보내는 인증번호입니다. 타인에게 노출 절대 금지. Yr60Qo0VGAq

위 처럼  무조건 <#> 형식으로 시작하고 Hash Key 로 끝나는 메시지 내용이어야만

Retriever API 가 메세지를 수신할 수 있습니다.

반드시 형식을 맞춰야 합니다.

1. Android Client 에서 Receiver 를 등록하고, SMS Retriever API 를 starting 하여 줍니다.

2. 사용자의 휴대폰 번호와 함께 11자리 Hash Key 를 구하여 백엔드 쪽 문자 발송 API에 전달하여 줍니다.

3. 백엔드 API 에서는 전달 받은 Hash Key 와 함께 메시지 양식을 제약조건에 맞춰 SMS 를 발송합니다.

4. Android Client 에서는 등록한 Receiver 로 SMS Message 를 수신하게 되고 이를 파싱하여 인증번호를 가져옵니다.

이상으로 포스팅을 마치겠습니다.

감사합니다.