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

使用 One Tap 登入用戶端要求權限,要求使用者擷取先前用於登入應用程式的憑證。這些憑證可以是 Google 帳戶或透過 Chrome、Android 自動填入功能或密碼專用 Smart Lock 與 Google 儲存的使用者名稱和密碼組合。

輕觸一下的登入 UI

成功擷取憑證後,即可讓使用者輕鬆登入應用程式。

如果使用者尚未儲存任何憑證,系統就不會顯示 UI,您可以提供一般的登出體驗。

哪裡適合使用 One Tap 登入功能?

如果應用程式需要使用者登入,請在登入畫面上顯示 One Tap UI。即使您已經擁有「使用 Google 帳戶登入」按鈕,這種做法也很有幫助:因為 One Tap UI 可以設定為只顯示使用者之前登入的憑證,也可以提醒使用者上次登入後不常登入的憑證,並防止使用者意外使用應用程式建立新帳戶。

如果應用程式不需要登入,建議您在任何具有強化體驗的畫面上使用 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。如需建議和產生 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. 檢查已登入的使用者

如果活動可供已登入的使用者或未登入的使用者使用,請在顯示 One Tap 登入使用者介面前檢查使用者的狀態。

您也應該要關閉提示或輕觸其他位置,藉此追蹤使用者是否已拒絕使用 One Tap 登入。這可以像 Activity 的布林值屬性一樣簡單。(請參閱下方的「停止顯示 One Tap UI」一節)。

3. 顯示 One Tap 登入 UI

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

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

如果使用者未儲存任何憑證,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. 處理使用者的回應

使用者對 One Tap 登入提示的回應,系統會使用活動的 onActivityResult() 方法向您的應用程式回報。如果使用者選擇登入,結果將會是已儲存的憑證。如果使用者拒絕登入 (關閉 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 UI

如果使用者拒絕登入,對 getSignInCredentialFromIntent() 的呼叫將會擲回含有 CommonStatusCodes.CANCELED 狀態碼的 ApiException。在此情況下,您應暫時停用 One Tap 登入 UI,以免重複提示使用者感到不悅。以下範例是在 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 帳戶的 Google ID 憑證。瞭解如何在後端使用這些權杖

如果您支援 Google 登入,也可以使用 One Tap 用戶端在應用程式中加入更流暢的帳戶建立流程