使用已儲存的憑證登入使用者

使用 One Tap 登入用戶端要求使用者授予權限,以擷取他們先前用來登入應用程式的其中一個憑證。這些憑證可以是 Google 帳戶或使用 Chrome、Android 自動填入或密碼專用 Smart Lock 儲存的使用者名稱和密碼組合。

輕觸一下的登入使用者介面

成功擷取憑證後,您可以使用這些憑證順利登入應用程式。

如果使用者尚未儲存任何憑證,系統並不會顯示任何使用者介面,而您仍可提供正常的登出體驗。

何時該使用 One Tap 登入?

如果應用程式要求使用者登入,請在登入畫面上顯示 One Tap 使用者介面。即使您已有「使用 Google 帳戶登入」按鈕,這項功能就能派上用場。由於將 One Tap 使用者介面設為只顯示使用者先前登入時使用的憑證,因此可以提醒使用者不常登入應用程式,並防止他們意外使用您的應用程式建立新帳戶。

如果應用程式可選用登入功能,請考慮在有助於提升體驗的任一螢幕上使用 One Tap 登入功能。舉例來說,如果使用者在登出狀態下使用您的應用程式瀏覽內容,但在登入之後,就只能張貼留言或將商品加入購物車,否則是 One Tap 登入的合理情境。

基於上述原因,登入裝置上的選用應用程式也應使用 One Tap 登入功能。

事前準備

1. 設定 One Tap 登入用戶端

您可以設定 One Tap 登入用戶端,使用已儲存的密碼和/或 Google 帳戶儲存使用者。(建議同時支援兩種做法,盡可能為新使用者啟用一鍵帳戶建立功能,並盡可能為回訪使用者啟用一鍵登入功能。)

如果您的應用程式使用密碼登入,請使用 setPasswordRequestOptions() 啟用密碼憑證要求。

如果您的應用程式使用 Google 登入功能,請使用 setGoogleIdTokenRequestOptions() 來啟用及設定 Google ID 權杖要求:

  • 將伺服器用戶端 ID 設為 [您在 Google API 控制台中建立的 ID]。請注意,這是指伺服器的用戶端 ID,而非 Android 用戶端 ID。

  • 設定用戶端以依授權帳戶進行篩選。啟用這個選項後,One Tap 用戶端只會提示使用者透過過去使用過的 Google 帳戶登入應用程式。如此可以確保使用者不確定自己是否已有帳戶,或他們使用的是哪個 Google 帳戶,並能避免使用者不小心在應用程式中建立新帳戶。

  • 如要盡可能讓使用者自動登入,請使用 setAutoSelectEnabled() 啟用這項功能。只要符合下列條件,即可自動登入:

    • 使用者只有一組為應用程式儲存的憑證,也就是儲存的密碼或已儲存的 Google 帳戶。
    • 使用者尚未在 Google 帳戶設定中停用自動登入功能。
  • 您可以視需要使用 Nonce 來改善登入安全性,以及避免重播攻擊。使用 setNonce 在每個要求中加入 Nonce。請參閱 SafetyNet's 取得 Nonce 一節,取得產生 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. 檢查已登入使用者

如果您的活動可供已登入的使用者或未登入的使用者使用,請先查看使用者的狀態,再顯示 One Tap 登入使用者介面。

您也必須關閉提示或輕觸提示外,藉此追蹤使用者是否已拒絕使用 One Tap 登入。這可以等同於活動的布林值屬性。(請參閱下方的停止顯示 One Tap 使用者介面)。

3. 顯示 One Tap 登入使用者介面

如果使用者未登入,且尚未拒絕使用 One Tap 登入,請呼叫用戶端物件 beginSignIn() 方法,並將事件監聽器附加至傳回的 Task。應用程式通常會在 Activity 的 onCreate() 方法中,或是在使用單一 Activity 架構的畫面轉換後進行這項操作。

如果使用者為應用程式儲存任何憑證,One Tap 用戶端會呼叫成功事件監聽器。在成功監聽器中,從 Task 結果取得待處理的意圖,並傳送至 startIntentSenderForResult() 以啟動 One Tap 登入使用者介面。

如果使用者沒有任何已儲存的憑證,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() 方法,回報使用者對 One Tap 登入提示的回應。如果使用者選擇登入,則結果會是已儲存的憑證。如果使用者拒絕登入 One Tap UI,或是在其外輕觸登入,結果就會傳回代碼 RESULT_CANCELED。您的應用程式必須處理兩種可能性。

使用已擷取的憑證登入

如果使用者選擇與應用程式共用憑證,您可以將意圖資料從 onActivityResult() 傳遞至 One Tap 用戶端的 getSignInCredentialFromIntent() 方法,藉此擷取憑證。如果使用者與應用程式共用 Google 帳戶憑證,則憑證將具有非空值的 googleIdToken 屬性,或如果使用者分享已儲存的密碼,則非 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) {
                    // ...
                }
            }
        }
    }
    // ...
}

停止顯示 One Tap 使用者介面

如果使用者拒絕登入,呼叫 getSignInCredentialFromIntent() 會擲回含有 CommonStatusCodes.CANCELED 狀態碼的 ApiException。如果發生這種情況,請暫時停用 One Tap 登入使用者介面,以免重複顯示提示。以下範例是透過在 Activity 上設定屬性來完成此操作,利用這個屬性判定是否提供使用者的 One Tap 登入功能。不過,您也可以將值儲存至 SharedPreferences 或使用其他方法。

請務必導入自己的 One Tap 登入提示頻率限制。 如果不這麼做,而使用者連續取消數則提示,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 ID 權杖。瞭解如何在後端使用這些權杖

如果支援 Google 登入功能,還可以使用 One Tap 用戶端在應用程式中新增流暢的帳戶建立流程