让用户使用已保存的凭据登录

使用一键式登录客户端向用户请求权限,以检索他们之前用于登录您应用的凭据之一。这些凭据可以是 Google 账号,也可以是用户使用 Chrome、Android 自动填充或密码智能锁定功能保存在 Google 中的用户名-密码组合。

一键登录界面

成功检索到凭据后,您可以使用这些凭据让用户轻松登录您的应用。

如果用户未保存任何凭据,系统不会显示任何界面,您可以提供正常的未登录体验。

在哪些位置可以使用一键登录?

如果您的应用需要用户登录,请在登录屏幕上显示一键式登录界面。即使您已经有“使用 Google 账号登录”按钮,这项功能也非常有用:由于“一键登录”界面可配置为仅显示用户之前用于登录的凭据,因此可以提醒不经常登录的用户上次是如何登录的,并防止他们意外使用您的应用创建新账号。

如果您的应用允许用户选择是否登录,请考虑在登录后可获得更好体验的任何屏幕上使用一键登录。例如,如果用户可以在未登录的情况下浏览应用中的内容,但只能在登录后发布评论或将商品添加到购物车,那么这便是使用一键式登录的合理情境。

出于上述原因,需要用户选择是否登录的应用也应在其登录屏幕上使用一键登录。

准备工作

1. 配置一键登录客户端

您可以配置一键登录客户端,让用户使用已保存的密码或已保存的 Google 账号登录,也可以同时使用这两种方式。(建议同时支持这两种方式,以便新用户一键创建账号,并尽可能为回访用户提供自动登录或一键登录方式。)

如果您的应用使用基于密码的登录方式,请使用 setPasswordRequestOptions() 启用密码凭据请求。

如果您的应用使用 Google 登录,请使用 setGoogleIdTokenRequestOptions() 启用和配置 Google ID 令牌请求:

  • 将服务器客户端 ID 设置为您在 Google API 控制台中创建的 ID。请注意,这是服务器的客户端 ID,而不是 Android 客户端 ID。

  • 将客户端配置为按已获授权的账号进行过滤。启用此选项后,一键登录客户端只会提示用户使用他们之前用过的 Google 账号登录您的应用。这样做有助于用户在不确定自己是否已有账号或使用的是哪个 Google 账号时成功登录,并可防止用户意外通过您的应用创建新账号。

  • 如果您希望尽可能让用户自动登录,请使用 setAutoSelectEnabled() 启用该功能。当满足以下条件时,用户就能自动登录:

    • 用户只有一个为您的应用保存的凭据。也就是说,一个已存的密码或一个已存的 Google 账号。
    • 用户尚未在其 Google 账号设置中停用自动登录功能。
  • 虽然 Nonce 并非必需,但我们强烈建议您考虑使用 Nonce 来提高登录安全性并避免重放攻击。使用 setNonce 在每个请求中添加 Nonce。如需有关生成 Nonce 的建议和更多详细信息,请参阅 SafetyNet 的获取 Nonce 部分。

Java

public class YourActivity extends AppCompatActivity {
  // ...

  private SignInClient oneTapClient;
  private BeginSignInRequest signInRequest;

  @Override
  public void onCreate(@Nullable Bundle savedInstanceState,
                       @Nullable PersistableBundle persistentState) {
      super.onCreate(savedInstanceState, persistentState);

      oneTapClient = Identity.getSignInClient(this);
      signInRequest = BeginSignInRequest.builder()
              .setPasswordRequestOptions(PasswordRequestOptions.builder()
                      .setSupported(true)
                      .build())
              .setGoogleIdTokenRequestOptions(GoogleIdTokenRequestOptions.builder()
                      .setSupported(true)
                      // Your server's client ID, not your Android client ID.
                      .setServerClientId(getString(R.string.default_web_client_id))
                      // Only show accounts previously used to sign in.
                      .setFilterByAuthorizedAccounts(true)
                      .build())
              // Automatically sign in when exactly one credential is retrieved.
              .setAutoSelectEnabled(true)
              .build();
      // ...
  }
  // ...
}

Kotlin

class YourActivity : AppCompatActivity() {
    // ...

    private lateinit var oneTapClient: SignInClient
    private lateinit var signInRequest: BeginSignInRequest

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        oneTapClient = Identity.getSignInClient(this)
        signInRequest = BeginSignInRequest.builder()
            .setPasswordRequestOptions(BeginSignInRequest.PasswordRequestOptions.builder()
                .setSupported(true)
                .build())
            .setGoogleIdTokenRequestOptions(
                BeginSignInRequest.GoogleIdTokenRequestOptions.builder()
                    .setSupported(true)
                    // Your server's client ID, not your Android client ID.
                    .setServerClientId(getString(R.string.your_web_client_id))
                    // Only show accounts previously used to sign in.
                    .setFilterByAuthorizedAccounts(true)
                    .build())
            // Automatically sign in when exactly one credential is retrieved.
            .setAutoSelectEnabled(true)
            .build()
        // ...
    }
    // ...
}

2. 检查是否有已登录的用户

如果您的 activity 可供已登录用户或未登录用户使用,请先检查用户的状态,然后再显示一键式登录界面。

您还应跟踪用户是否已关闭提示或点按提示以外的部分,以确定用户是否已拒绝使用一键式登录。这可以像 Activity 的布尔值属性一样简单。(请参阅下文中的停止显示一键式界面)。

3. 显示一键登录界面

如果用户未登录且尚未拒绝使用一键式登录,请调用客户端对象的 beginSignIn() 方法,并将监听器附加到它返回的 Task。应用通常会在 activity 的 onCreate() 方法中执行此操作,或者在使用单 activity 架构时在屏幕转换后执行此操作。

如果用户有任何已保存的应用凭据,One Tap 客户端将调用成功监听器。在成功监听器中,从 Task 结果中获取待处理 intent,并将其传递给 startIntentSenderForResult() 以启动 One Tap 登录界面。

如果用户没有任何已保存的凭据,One Tap 客户端将调用失败监听器。在这种情况下,无需执行任何操作:您只需继续呈现应用的未登录体验即可。不过,如果您支持一键式注册,可以在此处启动该流程,获得流畅的账号创建体验。请参阅一键创建新账号

Java

oneTapClient.beginSignIn(signUpRequest)
        .addOnSuccessListener(this, new OnSuccessListener<BeginSignInResult>() {
            @Override
            public void onSuccess(BeginSignInResult result) {
                try {
                    startIntentSenderForResult(
                            result.getPendingIntent().getIntentSender(), REQ_ONE_TAP,
                            null, 0, 0, 0);
                } catch (IntentSender.SendIntentException e) {
                    Log.e(TAG, "Couldn't start One Tap UI: " + e.getLocalizedMessage());
                }
            }
        })
        .addOnFailureListener(this, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                // No saved credentials found. Launch the One Tap sign-up flow, or
                // do nothing and continue presenting the signed-out UI.
                Log.d(TAG, e.getLocalizedMessage());
            }
        });

Kotlin

oneTapClient.beginSignIn(signInRequest)
    .addOnSuccessListener(this) { result ->
        try {
            startIntentSenderForResult(
                result.pendingIntent.intentSender, REQ_ONE_TAP,
                null, 0, 0, 0, null)
        } catch (e: IntentSender.SendIntentException) {
            Log.e(TAG, "Couldn't start One Tap UI: ${e.localizedMessage}")
        }
    }
    .addOnFailureListener(this) { e ->
        // No saved credentials found. Launch the One Tap sign-up flow, or
        // do nothing and continue presenting the signed-out UI.
        Log.d(TAG, e.localizedMessage)
    }

4. 处理用户的响应

系统会使用 activity 的 onActivityResult() 方法将用户对一键式登录提示的响应报告给您的应用。如果用户选择登录,结果将是已保存的凭据。如果用户拒绝登录(通过关闭一键式登录界面或点按界面之外的区域),系统会返回代码 RESULT_CANCELED。您的应用需要处理这两种情况。

使用检索到的凭据登录

如果用户选择与您的应用共享凭据,您可以通过将 intent 数据从 onActivityResult() 传递给 One Tap 客户端的 getSignInCredentialFromIntent() 方法来检索这些凭据。如果用户与您的应用分享了 Google 账号凭据,凭据将具有非 null 的 googleIdToken 属性;如果用户分享了已保存的密码,凭据将具有非 null 的 password 属性。

使用该凭据对应用的后端进行身份验证。

  • 如果检索到用户名和密码对,请使用它们登录,就像用户手动提供这些信息一样。
  • 如果检索到 Google 账号凭据,请使用 ID 令牌对您的后端进行身份验证。如果您选择使用 Nonce 来帮助避免重放攻击,请检查后端服务器上的响应值。请参阅使用 ID 令牌与后端进行身份验证

Java

public class YourActivity extends AppCompatActivity {

  // ...
  private static final int REQ_ONE_TAP = 2;  // Can be any integer unique to the Activity.
  private boolean showOneTapUI = true;
  // ...

  @Override
  protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
      super.onActivityResult(requestCode, resultCode, data);

      switch (requestCode) {
          case REQ_ONE_TAP:
              try {
                  SignInCredential credential = oneTapClient.getSignInCredentialFromIntent(data);
                  String idToken = credential.getGoogleIdToken();
                  String username = credential.getId();
                  String password = credential.getPassword();
                  if (idToken !=  null) {
                      // Got an ID token from Google. Use it to authenticate
                      // with your backend.
                      Log.d(TAG, "Got ID token.");
                  } else if (password != null) {
                      // Got a saved username and password. Use them to authenticate
                      // with your backend.
                      Log.d(TAG, "Got password.");
                  }
              } catch (ApiException e) {
                  // ...
              }
              break;
      }
  }
}

Kotlin

class YourActivity : AppCompatActivity() {

    // ...
    private val REQ_ONE_TAP = 2  // Can be any integer unique to the Activity
    private var showOneTapUI = true
    // ...

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) {
             REQ_ONE_TAP -> {
                try {
                    val credential = oneTapClient.getSignInCredentialFromIntent(data)
                    val idToken = credential.googleIdToken
                    val username = credential.id
                    val password = credential.password
                    when {
                        idToken != null -> {
                            // Got an ID token from Google. Use it to authenticate
                            // with your backend.
                            Log.d(TAG, "Got ID token.")
                        }
                        password != null -> {
                            // Got a saved username and password. Use them to authenticate
                            // with your backend.
                            Log.d(TAG, "Got password.")
                        }
                        else -> {
                            // Shouldn't happen.
                            Log.d(TAG, "No ID token or password!")
                        }
                    }
                } catch (e: ApiException) {
                    // ...
                }
            }
        }
    }
    // ...
}

停止显示一键登录界面

如果用户拒绝登录,对 getSignInCredentialFromIntent() 的调用将抛出状态代码为 CommonStatusCodes.CANCELEDApiException。在这种情况下,您应暂时停用一键式登录界面,以免重复提示给用户带来困扰。以下示例通过在 activity 上设置一个属性来实现此目的,该属性用于确定是否向用户提供一键式登录;不过,您也可以将值保存到 SharedPreferences 或使用其他方法。

请务必实现您自己的“一键式登录”提示速率限制。如果您不设置此属性,并且用户连续取消多次提示,One Tap 客户端在接下来的 24 小时内将不会向用户显示提示。

Java

public class YourActivity extends AppCompatActivity {

  // ...
  private static final int REQ_ONE_TAP = 2;  // Can be any integer unique to the Activity.
  private boolean showOneTapUI = true;
  // ...

  @Override
  protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
      super.onActivityResult(requestCode, resultCode, data);

      switch (requestCode) {
          case REQ_ONE_TAP:
              try {
                  // ...
              } catch (ApiException e) {
                  switch (e.getStatusCode()) {
                      case CommonStatusCodes.CANCELED:
                          Log.d(TAG, "One-tap dialog was closed.");
                          // Don't re-prompt the user.
                          showOneTapUI = false;
                          break;
                      case CommonStatusCodes.NETWORK_ERROR:
                          Log.d(TAG, "One-tap encountered a network error.");
                          // Try again or just ignore.
                          break;
                      default:
                          Log.d(TAG, "Couldn't get credential from result."
                                  + e.getLocalizedMessage());
                          break;
                  }
              }
              break;
      }
  }
}

Kotlin

class YourActivity : AppCompatActivity() {

    // ...
    private val REQ_ONE_TAP = 2  // Can be any integer unique to the Activity
    private var showOneTapUI = true
    // ...

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        when (requestCode) {
            REQ_ONE_TAP -> {
                try {
                    // ...
                } catch (e: ApiException) {
                    when (e.statusCode) {
                        CommonStatusCodes.CANCELED -> {
                            Log.d(TAG, "One-tap dialog was closed.")
                            // Don't re-prompt the user.
                            showOneTapUI = false
                        }
                        CommonStatusCodes.NETWORK_ERROR -> {
                            Log.d(TAG, "One-tap encountered a network error.")
                            // Try again or just ignore.
                        }
                        else -> {
                            Log.d(TAG, "Couldn't get credential from result." +
                                " (${e.localizedMessage})")
                        }
                    }
                }
            }
        }
    }
    // ...
}

5. 处理退出账号操作

当用户从应用中退出账号时,请调用 One Tap 客户端的 signOut() 方法。调用 signOut() 会停用自动登录,直到用户重新登录。

即使您不使用自动登录,此步骤也非常重要,因为它可确保当用户从您的应用中退出账号时,您使用的所有 Play 服务 API 的身份验证状态也会重置。

后续步骤

如果您配置了 One Tap 客户端以检索 Google 凭据,您的应用现在可以获取代表用户 Google 账号的 Google ID 令牌。了解如何在后端使用这些令牌

如果您支持 Google 登录,还可以使用 One Tap 客户端为您的应用添加顺畅的账号创建流程