如何在Android中使用SMS Retriever API进行自动短信验证

1,508 阅读7分钟

在Android中使用SMS Retriever API进行自动短信验证

自动短信验证可以在一个叫做SMS Retriever API 的API的帮助下完成。使用这个API,用户不需要手动输入验证码,API也不需要任何额外的应用权限。

在本教程中,我们将学习如何在Android应用中实现这一功能。

前提条件

要继续学习本教程,读者应该。

  • 掌握了 Kotlin编程语言。
  • 知道如何在Android studio中使用XML 来设计布局。
  • Android Broadcasts 有一定的了解。

目标

在本教程结束时,读者应该明白。

  • 什么是短信验证过程。
  • 如何在你的应用程序中使用自动短信验证功能。

什么是SMS Retriever API?

SMS Retriever是一个API,允许你验证用户的短信而不强迫他们输入验证码。通过这个API,你可以为你的应用程序提取验证码。这是在不要求完整的短信阅读权限的情况下完成的。

当用户设备收到一条信息时,谷歌游戏服务会检查应用程序的哈希值。然后,它通过SMS Retriever API将消息文本发送给你的应用程序。然后该应用程序读取并提取短信中的代码。这个代码通常被送回服务器进行验证。

短信验证过程

对于手机号码验证,你需要首先实现客户端。之后,在服务器端,完成验证程序。通常,你把用户的电话号码发送到执行验证的服务器。然后服务器向所提供的电话号码发送一个OTP(一次性密码)代码。

SMS Retriever API监听含有OTP代码的短信。收到代码后,它将其发送回服务器以完成验证过程。

为什么使用自动SMS Retriever API?

  • 谷歌废除了所有使用CALL_LOGREAD_SMS 权限的应用程序。这是因为它们侵犯了用户的隐私。这导致在2021年1月19日将使用这些权限的应用程序从play store移除。
  • 它提供了一个更顺畅和毫不费力的用户体验。

第1步:创建一个新的Android studio项目

Create Project

第2步:添加必要的依赖项

我们将使用以下内容。

  • Apache Commons - 这个库将帮助我们从SMS信息中提取代码。
  • Google Play Services API - 这个库持有短信检索类。
  • EventBus - 为了监听来自短信检索API的接收短信,我们将使用BroadcastReceiver。EventBus是一个发布者/订阅者模式库。我们用它来在我们的BroadcastReceiver和Activity类之间进行通信。

将这些添加到build.gradle文件中并同步项目。

implementation 'org.apache.commons:commons-lang3:3.11'
implementation 'com.google.android.gms:play-services-auth:19.2.0'
implementation 'org.greenrobot:eventbus:3.2.0'

第3步:为我们的项目设置XML布局

我们将在这部分创建一个编辑文本。这个编辑文本将显示从我们的SMS消息中获得的一次性代码。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

        <EditText
            android:id="@+id/editText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textAlignment="center"
            android:inputType="number"
            android:layout_marginTop="80dp"
            android:layout_marginStart="30dp"
            android:layout_marginEnd="30dp"/>
</LinearLayout>

发送手机号码到服务器

在这一步,你必须从EditText 。把它发送到你的验证服务器,它应该返回一次性代码。因为我还没有一个验证服务器,所以我们在这篇文章中不打算使用这个方法。我们将从另一部手机发送短信。该短信将包含一个四位数的代码。

这个代码将被提取并显示在我们在activity_main.xml 中添加的EditText上。

第4步:获取SmsRetriverClient的实例

我们首先要获得一个SmsRetrieverClient的实例。接着调用initSmsRetriever实例函数,并将onSuccessListeneronFailureListener 加入到任务中。我们把所有这些都包在一个函数中。

private fun initSmsListener() {
    smsClient.startSmsRetriever()
        .addOnSuccessListener {
            //You can perform your tasks here
        }.addOnFailureListener { failure ->
            failure.printStackTrace()
            Toast.makeText(this, failure.message, Toast.LENGTH_SHORT).show()
        }
}

上述函数在onCreate() 方法中被调用。

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var smsClient: SmsRetrieverClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        smsClient = SmsRetriever.getClient(this)
        
        initSmsListener()
    }

我们的API将向应用程序传输一个SmsRetriever.SMS RETRIEVED ACTION 意向。这发生在设备收到包含代码的消息时。这个意图持有短信信息和后台处理状态。

为了处理这个问题,我们将创建一个BroadcastReceiver类。

class MessageBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (SmsRetriever.SMS_RETRIEVED_ACTION == intent?.action) {
            val data = intent.extras
            if (data != null) {
                val status = data[SmsRetriever.EXTRA_STATUS] as Status
                var timedOut = false
                var otpCode: String? = null

                when (status.statusCode) {
                    CommonStatusCodes.SUCCESS -> {
                        val appMessage = data[SmsRetriever.EXTRA_SMS_MESSAGE] as String
                        otpCode = appMessage
                    }
                    CommonStatusCodes.TIMEOUT -> {
                        timedOut = true
                    }
                }
                EventBus.getDefault().post(RetrievalEvent(timedOut, otpCode.toString()))
            }
        }
    }
}

onReceive() 方法上,首先我们检查SMS Retriever 背景处理的状态。我们还构建一个RetrievalEvent 类的实例。这是一个事件类,EventBus 将发送至我们的SubscriberRetrievalEvent 这个类将是一个数据类。

RetrievalEvent

data class RetrievalEvent (
    val timedOut: Boolean,
    val message: String
    )

这个数据类的属性被设置为检索到的SMS消息。这是在后台处理成功的情况下进行的。如果在5分钟内没有收到消息,通常会发生超时。如果它发生,超时被设置为真。然后,将事件发送给监听用户。

第5步:在Android Manifest上注册BroadcastReceiver

在你的应用程序的AndroidManifest.xml 文件中,注册BroadcastReceiver

<?xml version="1.0" encoding="utf-8"?>
<manifest 
    ...     >

    <application
        ...     >

        <receiver
            android:name=".MessageBroadcastReceiver"
            android:exported="true">
            <intent-filter>
                <action android:name="com.google.android.gms.auth.api.phone.SMS_RETRIEVED"/>
            </intent-filter>
        </receiver>
    </application>

</manifest>

接下来,在我们的MainActivity class ,我们将注册、取消注册,并实现我们的订阅者。当一个事件被发布时,onReceiveSms() 方法将被调用。它通常被注解为@Subscribe 注解。

注册和取消注册的接收者通常分别在onStart()onStop() 方法上完成。substringAfterLast() 函数被用来提取通过SMS发送的代码。

注意:一定要记得注册和取消注册成员以避免内存泄漏。

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding
    private lateinit var smsClient: SmsRetrieverClient
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        smsClient = SmsRetriever.getClient(this)

        //uncomment this to generate your app hash string. You can view the hash string on your log cat when you run the app
        /* val appSignatureHelper = SignatureHelper(this)
        Log.d("SIGNATURE",appSignatureHelper.appSignature.toString())*/

        initSmsListener()
    }
    
     override fun onStart() {
        super.onStart()
        EventBus.getDefault().register(this)
    }

    override fun onStop() {
        EventBus.getDefault().unregister(this)
        super.onStop()
    }
    
    private fun initSmsListener() {
        smsClient.startSmsRetriever()
            .addOnSuccessListener {
                Toast.makeText(
                    this, "Waiting for sms message",
                    Toast.LENGTH_SHORT
                ).show()
            }.addOnFailureListener { failure ->
                Toast.makeText(
                    this, failure.localizedMessage,
                    Toast.LENGTH_SHORT
                ).show()
            }
    }

    @Subscribe
    fun onReceiveSms(retrievalEvent: RetrievalEvent) {
        val code: String =
            StringUtils.substringAfterLast(retrievalEvent.message, "is").replace(":", "")
                .trim().substring(0, 4)

        runOnUiThread {
            if (!retrievalEvent.timedOut) {
                binding.editText.setText(code)
            } else {
                Toast.makeText(this, "Failed", Toast.LENGTH_SHORT).show()
            }
        }
        initSmsListener()
    }
}

计算你的应用程序的哈希字符串

为了生成哈希字符串,你可以使用以下方法。

  • 使用Play App Signing
  • 使用SignatureHelper class 。这个类将有助于生成我们应用程序的哈希字符串。在使用这个类获得哈希字符串后,一定要将其删除。
    /**
     * This is a helper class to generate your message hash to be included in your SMS message.
     *
     * Without the correct hash, your app won't receive the message callback. This only needs to be
     * generated once per app and stored. Then you can remove this helper class from your code.
     */
class SignatureHelper(context: Context?) :
    ContextWrapper(context) {
    // For each signature create a compatible hash
     /**
       * Get all the app signatures for the current package
       */
    val appSignature: ArrayList<String>
        get() {
            val appCodes = ArrayList<String>()
            try {
                // Get all package signatures for the current package
                val myPackageName = packageName
                val myPackageManager = packageManager
                val signatures = myPackageManager.getPackageInfo(myPackageName,PackageManager.GET_SIGNATURES).signatures                                      
                // For each signature create a compatible hash
                for (signature in signatures) {
                    val hash = hash(myPackageName, signature.toCharsString())                      
                    if (hash != null) {
                        appCodes.add(String.format("%s", hash))
                    }
                }
            } catch (e: PackageManager.NameNotFoundException) {
                Log.d(TAG,"Package not found",e)                             
            }
            return appCodes
        }

    companion object {
        private const val HASH_TYPE = "SHA-256"
        const val HASHED_BYTES = 9
        const val BASE64_CHAR = 11
        private fun hash(pkgName: String, signature: String): String? {
            val appInfo = "$pkgName $signature"
            try {
                val messageDigest =  MessageDigest.getInstance(HASH_TYPE)                   
                messageDigest.update(appInfo.toByteArray(StandardCharsets.UTF_8))
                var myHashSignature = messageDigest.digest()
                // truncated into HASHED_BYTES
                myHashSignature = Arrays.copyOfRange(myHashSignature,0,HASHED_BYTES)                                                                       
                // encode into Base64
                var base64Hash = Base64.encodeToString(myHashSignature,Base64.NO_PADDING or Base64.NO_WRAP)                                          
                base64Hash = base64Hash.substring(0, BASE64_CHAR)
                Log.d(TAG, String.format("pkg: %s -- hash: %s", pkgName, base64Hash))                             
                return base64Hash
            } catch (error: NoSuchAlgorithmException) {
                Log.e(TAG, "Algorithm not Found", error)
            }
            return null
        }
    }
}

最后,请记住,你应该在你的消息上使用如下格式。

  • 信息应该小于140字节。
  • 该消息应该有OTP代码。
  • 你的消息应该以你的应用程序的11个字符的哈希字符串结束。

以下是一个例子

Your Sms Retriever Api code is: 6647
u0tUcRo4UQ7

演示屏幕

运行该应用程序时,可以看到以下内容。

Screen One

结论

Automatic Retriever API是一个有助于检测和提取OTP代码的库。这个代码通常被送回服务器进行验证。这个API执行任务时不需要用户为应用程序提供权限。这使得用户的入职体验变得顺畅和吸引人。