在 Android 应用中请求短信验证

如需自动验证电话号码,您必须同时实现验证流程的客户端和服务器部分。本文档介绍了如何在 Android 应用中实现客户端部分。

如需在 Android 应用中启动电话号码验证流程,请将电话号码发送到验证服务器,并调用 SMS Retriever API 开始监听包含应用的一次性验证码的短信。收到短信后,您需要将一次性验证码发送回服务器,以完成验证流程。

准备工作

为了让您的应用做好准备,请完成以下部分中的步骤。

应用要满足的前提条件

确保您应用的 build 文件使用以下值:

  • minSdkVersion 为 19 或更高
  • 28 或更高版本的 compileSdkVersion

配置您的应用

在您的项目级 build.gradle 文件中,同时在 buildscriptallprojects 两个部分中添加 Google 的 Maven 代码库Maven 中央代码库

buildscript {
    repositories {
        google()
        mavenCentral()
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

将 SMS Retriever API 的 Google Play 服务依赖项添加到模块的 Gradle build 文件(通常为 app/build.gradle):

dependencies {
  implementation 'com.google.android.gms:play-services-auth:20.2.0'
  implementation 'com.google.android.gms:play-services-auth-api-phone:18.0.1'
}

1. 获取用户的电话号码

您可以通过任何适合您的应用的方式获取用户的电话号码。通常情况下,最好使用提示选择器来提示用户从设备上存储的电话号码,从而避免手动输入电话号码。如需使用提示选择器,请执行以下操作:

// Construct a request for phone numbers and show the picker
private void requestHint() {
    HintRequest hintRequest = new HintRequest.Builder()
           .setPhoneNumberIdentifierSupported(true)
           .build();

    PendingIntent intent = Auth.CredentialsApi.getHintPickerIntent(
            apiClient, hintRequest);
    startIntentSenderForResult(intent.getIntentSender(),
            RESOLVE_HINT, null, 0, 0, 0);
}

// Obtain the phone number from the result
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  if (requestCode == RESOLVE_HINT) {
      if (resultCode == RESULT_OK) {
          Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY);
          // credential.getId();  <-- will need to process phone number string
      }
  }
}

2. 启动短信检索器

当您准备好验证用户的电话号码时,请获取 SmsRetrieverClient 对象的实例、调用 startSmsRetriever,并将成功和失败的监听器附加到短信检索任务:

// Get an instance of SmsRetrieverClient, used to start listening for a matching
// SMS message.
SmsRetrieverClient client = SmsRetriever.getClient(this /* context */);

// Starts SmsRetriever, which waits for ONE matching SMS message until timeout
// (5 minutes). The matching SMS message will be sent via a Broadcast Intent with
// action SmsRetriever#SMS_RETRIEVED_ACTION.
Task<Void> task = client.startSmsRetriever();

// Listen for success/failure of the start Task. If in a background thread, this
// can be made blocking using Tasks.await(task, [timeout]);
task.addOnSuccessListener(new OnSuccessListener<Void>() {
  @Override
  public void onSuccess(Void aVoid) {
    // Successfully started retriever, expect broadcast intent
    // ...
  }
});

task.addOnFailureListener(new OnFailureListener() {
  @Override
  public void onFailure(@NonNull Exception e) {
    // Failed to start retriever, inspect Exception for more details
    // ...
  }
});

短信检索任务最多会等待五分钟,短信中包含标识您的应用的唯一字符串。

3.将电话号码发送到您的服务器

获取用户的电话号码并开始监听短信后,使用任何方法(通常通过 HTTPS POST 请求)将用户的电话号码发送到验证服务器。

您的服务器生成验证消息,并通过短信将其发送到您指定的电话号码。请参阅在服务器上执行短信验证

4.接收验证邮件

用户设备上收到验证消息后,Play 服务会明确向您的应用广播一个包含该消息文本的 SmsRetriever.SMS_RETRIEVED_ACTION Intent。请使用 BroadcastReceiver 接收此验证消息。

BroadcastReceiveronReceive 处理程序中,从 Intent 的 extra 中获取验证消息的文本:

/**
 * BroadcastReceiver to wait for SMS messages. This can be registered either
 * in the AndroidManifest or at runtime.  Should filter Intents on
 * SmsRetriever.SMS_RETRIEVED_ACTION.
 */
public class MySMSBroadcastReceiver extends BroadcastReceiver {

  @Override
  public void onReceive(Context context, Intent intent) {
    if (SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) {
      Bundle extras = intent.getExtras();
      Status status = (Status) extras.get(SmsRetriever.EXTRA_STATUS);

      switch(status.getStatusCode()) {
        case CommonStatusCodes.SUCCESS:
          // Get SMS message contents
          String message = (String) extras.get(SmsRetriever.EXTRA_SMS_MESSAGE);
          // Extract one-time code from the message and complete verification
          // by sending the code back to your server.
          break;
        case CommonStatusCodes.TIMEOUT:
          // Waiting for SMS timed out (5 minutes)
          // Handle the error ...
          break;
      }
    }
  }
}

在应用的 AndroidManifest.xml 文件中使用 intent 过滤器 com.google.android.gms.auth.api.phone.SMS_RETRIEVEDSmsRetriever.SMS_RETRIEVED_ACTION 常量的值)和权限 com.google.android.gms.auth.api.phone.permission.SENDSmsRetriever.SEND_PERMISSION 常量的值)注册此 BroadcastReceiver(如下例所示),或者使用 Context.registerReceiver 动态注册。

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

5. 将验证邮件中的一次性验证码发送到您的服务器

现在您已经获得了验证消息的文本,接下来可以使用正则表达式或其他一些逻辑从消息中获取一次性代码。一次性代码的格式取决于您在服务器中实现这些代码的方式。

最后,通过安全连接将一次性代码发送到您的服务器。当您的服务器收到一次性验证码时,它会记录该手机号码已通过验证。

可选:使用 Smart Lock(密码专用)保存电话号码

(可选)在用户验证其电话号码后,您可以提示用户使用 Smart Lock(密码专用)保存此电话号码帐号,使其自动显示在其他应用和其他设备中,而无需再次输入或选择相应电话号码:

Credential credential = new Credential.Builder(phoneNumberString)
        .setAccountType("https://signin.example.com")  // a URL specific to the app
        .setName(displayName)  // optional: a display name if available
        .build();
Auth.CredentialsApi.save(apiClient, credential).setResultCallback(
            new ResultCallback() {
                public void onResult(Result result) {
                    Status status = result.getStatus();
                    if (status.isSuccess()) {
                        Log.d(TAG, "SAVE: OK");  // already saved
                    } else if (status.hasResolution()) {
                        // Prompt the user to save
                        status.startResolutionForResult(this, RC_SAVE);
                    }
                }
            });

然后,当用户重新安装应用或在新设备上安装该应用后,您可以检索已保存的电话号码,而无需再次要求用户提供电话号码:

// On the next install, retrieve the phone number
mCredentialRequest = new CredentialRequest.Builder()
    .setAccountTypes("https://signin.example.com")  // the URL specific to the developer
    .build();
Auth.CredentialsApi.request(apiClient, mCredentialRequest).setResultCallback(
    new ResultCallback<CredentialRequestResult>() {
        public void onResult(CredentialRequestResult credentialRequestResult) {
            if (credentialRequestResult.getStatus().isSuccess()) {
                credentialRequestResult.getCredential().getId();  // this is the phone number
            }
        }
    });

// Then, initiate verification and sign the user in (same as original verification logic)