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

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

輕觸一下即可登入的 UI

成功擷取憑證後,您就能使用這些憑證,讓使用者順利登入應用程式。

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

我應該在何處使用 One Tap 登入?

如果應用程式要求使用者登入,請在登入畫面上顯示「一鍵登入」UI。即使您已有「使用 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 的「取得隨機數」一節。

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 登入」UI。

此外,您也應追蹤使用者是否已拒絕使用「One Tap 登入」功能 (方法是關閉提示或輕觸提示以外的區域)。這可以像活動的布林屬性一樣簡單。(請參閱下方的「停止顯示 One Tap 使用者介面」一節)。

3. 顯示 One Tap 登入 UI

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

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

如果使用者沒有任何已儲存的憑證,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 或輕觸 UI 外部),結果會傳回 RESULT_CANCELED 代碼。應用程式必須能處理這兩種情況。

使用復原的憑證登入

如果使用者選擇與應用程式共用憑證,您可以將意圖資料從 onActivityResult() 傳遞至 One Tap 用戶端的 getSignInCredentialFromIntent() 方法,藉此擷取憑證。如果使用者與應用程式共用 Google 帳戶憑證,憑證就會有非空值的 googleIdToken 屬性;如果使用者共用已儲存的密碼,憑證就會有非空值的 password 屬性。

使用憑證向應用程式後端進行驗證。

  • 如果系統已擷取使用者名稱和密碼組合,請使用這些資訊登入,登入方式與使用者手動提供資訊時相同。
  • 如果已擷取 Google 帳戶憑證,請使用 ID 權杖向後端進行驗證。如果您選擇使用隨機數來避免重送攻擊,請檢查後端伺服器上的回應值。請參閱「使用 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() 的呼叫會擲回 ApiException,並傳回 CommonStatusCodes.CANCELED 狀態碼。 發生這種情況時,您應暫時停用「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 帳戶的 Google ID 權杖。瞭解如何在後端使用這些權杖

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