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

使用一键登录客户端请求用户授予权限,以检索他们之前用于登录您应用的凭据之一。这些凭据可以是 Google 账号,也可以是他们使用 Chrome、Android 自动填充功能或 Smart Lock 密码管理工具保存到 Google 的用户名/密码组合。

一键登录界面

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

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

在哪些情况下应使用一键登录?

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

如果您的应用不强制要求登录,请考虑在任何可通过登录来提升体验的界面上使用一键登录。例如,如果用户可以在未登录的情况下使用您的应用浏览内容,但只有在登录后才能发表评论或将商品添加到购物车,那么这就是一键登录的合理使用场景。

出于同样的原因,登录可选应用也应在其登录界面上使用一键登录。

准备工作

1. 配置一键登录客户端

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

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

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

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

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

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

    • 用户只有一个为您的应用保存的凭据。也就是说,一个已存的密码或一个已存的 Google 账号。
    • 用户尚未在其 Google 账号设置中停用自动登录功能。
  • 虽然这是可选操作,但我们强烈建议您考虑使用 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 架构时在屏幕转换后执行此操作。

  • 找到已保存的凭据:如果用户有任何已保存的应用凭据,一键登录客户端将调用成功监听器。在成功监听器中,从 Task 结果中获取待处理 intent,并将其传递给 startIntentSenderForResult() 以启动一键登录界面。
  • 未找到已保存的凭据:一键登录客户端将调用失败监听器。在这种情况下,您无需采取任何措施:您可以继续呈现应用的退出登录体验。不过,如果您支持一键注册,则可以在此处启动该流程,以提供顺畅的账号创建体验。请参阅一键创建新账号。

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() 传递到一键快捷登录客户端的 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() 的调用将抛出 ApiException,并返回 CommonStatusCodes.CANCELED 状态代码。发生这种情况时,您应暂时停用一键登录界面,以免重复提示让用户感到厌烦。以下示例通过在 Activity 上设置一个属性来实现此目的,该属性用于确定是否向用户提供一键登录功能;不过,您也可以将值保存到 SharedPreferences 或使用其他方法。

请务必自行实现一键登录提示的速率限制。 如果您不这样做,并且用户连续取消了多个提示,则一键快捷登录客户端在接下来的 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. 处理退出账号操作

当用户退出您的应用时,请调用一键式客户端的 signOut() 方法。调用 signOut() 会停用自动登录功能,直到用户再次登录。

即使您不使用自动登录,此步骤也很重要,因为它可确保当用户退出您的应用时,您使用的任何 Play 服务 API 的身份验证状态也会重置。

后续步骤

如果您将一键登录客户端配置为检索 Google 凭证,您的应用现在可以获取代表用户 Google 账号的 Google ID 令牌。了解如何在后端使用这些令牌。

如果您支持 Google 登录,还可以使用一键式客户端向应用添加顺畅的账号创建流程。