Android 適用的已連結帳戶登入功能

已連結帳戶登入功能可讓已將 Google 帳戶連結至您服務的使用者使用 One Tap 登入 Google。這樣一來,使用者只要按一下滑鼠就能登入,不必重新輸入使用者名稱和密碼,使用體驗也因此獲得改善。這麼做也可以降低使用者在您的服務中建立重複帳戶的可能性。

連結帳戶登入功能可在 Android 裝置的 One Tap 登入流程中使用。也就是說,如果應用程式已啟用 One Tap 功能,您就不需要匯入獨立的程式庫。

本文將說明如何修改 Android 應用程式,以支援已連結帳戶登入功能。

運作方式

  1. 您可以選擇在 One Tap 登入流程中顯示已連結的帳戶。
  2. 如果使用者已登入 Google,並將自己的 Google 帳戶連結至您的服務帳戶,系統會針對已連結的帳戶傳回 ID 權杖。
  3. 系統會向使用者顯示 One Tap 登入提示,讓他們選擇以連結的帳戶登入服務。
  4. 如果使用者選擇繼續使用已連結的帳戶,系統會將使用者的 ID 權杖傳回至您的應用程式。您可以將此權杖與步驟 2 中傳送至伺服器的權杖比對,以便識別已登入的使用者。

設定

設定開發環境

在開發主機上取得最新的 Google Play 服務:

  1. 開啟 Android SDK Manager
  1. 在「SDK Tools」下方,找出「Google Play 服務」

  2. 如果這些套件的狀態不是「已安裝」,請選取這兩個套件,然後按一下「安裝套件」

設定應用程式

  1. 在專案層級 build.gradle 檔案中,請同時在 buildscriptallprojects 區段中納入 Google 的 Maven 存放區。

    buildscript {
        repositories {
            google()
        }
    }
    
    allprojects {
        repositories {
            google()
        }
    }
    
  2. 將「Link with Google」API 的依附元件新增至模組的應用程式層級 Gradle 檔案,通常為 app/build.gradle

    dependencies {
      implementation 'com.google.android.gms:play-services-auth:21.3.0'
    }
    

修改 Android 應用程式,支援已連結帳戶登入功能

在「已連結帳戶登入」流程結束時,系統會將 ID 權杖傳回至您的應用程式。在讓使用者登入前,應先驗證 ID 權杖的完整性。

以下程式碼範例詳細說明擷取、驗證 ID 權杖的步驟,以及隨後讓使用者登入的步驟。

  1. 建立活動來接收登入意圖的結果

    Kotlin

      private val activityResultLauncher = registerForActivityResult(
        ActivityResultContracts.StartIntentSenderForResult()) { result ->
        if (result.resultCode == RESULT_OK) {
          try {
            val signInCredentials = Identity.signInClient(this)
                                    .signInCredentialFromIntent(result.data)
            // Review the Verify the integrity of the ID token section for
            // details on how to verify the ID token
            verifyIdToken(signInCredential.googleIdToken)
          } catch (e: ApiException) {
            Log.e(TAG, "Sign-in failed with error code:", e)
          }
        } else {
          Log.e(TAG, "Sign-in failed")
        }
      }
    

    Java

      private final ActivityResultLauncher<IntentSenderResult>
        activityResultLauncher = registerForActivityResult(
        new ActivityResultContracts.StartIntentSenderForResult(),
        result -> {
        If (result.getResultCode() == RESULT_OK) {
            try {
              SignInCredential signInCredential = Identity.getSignInClient(this)
                             .getSignInCredentialFromIntent(result.getData());
              verifyIdToken(signInCredential.getGoogleIdToken());
            } catch (e: ApiException ) {
              Log.e(TAG, "Sign-in failed with error:", e)
            }
        } else {
            Log.e(TAG, "Sign-in failed")
        }
    });
    
  2. 建立登入要求

    Kotlin

    private val tokenRequestOptions =
    GoogleIdTokenRequestOptions.Builder()
      .supported(true)
      // Your server's client ID, not your Android client ID.
      .serverClientId(getString("your-server-client-id")
      .filterByAuthorizedAccounts(true)
      .associateLinkedAccounts("service-id-of-and-defined-by-developer",
                               scopes)
      .build()
    

    Java

     private final GoogleIdTokenRequestOptions tokenRequestOptions =
         GoogleIdTokenRequestOptions.Builder()
      .setSupported(true)
      .setServerClientId("your-service-client-id")
      .setFilterByAuthorizedAccounts(true)
      .associateLinkedAccounts("service-id-of-and-defined-by-developer",
                                scopes)
      .build()
    
  3. 啟動待處理登入意圖

    Kotlin

     Identity.signInClient(this)
        .beginSignIn(
      BeginSignInRequest.Builder()
        .googleIdTokenRequestOptions(tokenRequestOptions)
      .build())
        .addOnSuccessListener{result ->
          activityResultLauncher.launch(result.pendingIntent.intentSender)
      }
      .addOnFailureListener {e ->
        Log.e(TAG, "Sign-in failed because:", e)
      }
    

    Java

     Identity.getSignInClient(this)
      .beginSignIn(
        BeginSignInRequest.Builder()
          .setGoogleIdTokenRequestOptions(tokenRequestOptions)
          .build())
      .addOnSuccessListener(result -> {
        activityResultLauncher.launch(
            result.getPendingIntent().getIntentSender());
    })
    .addOnFailureListener(e -> {
      Log.e(TAG, "Sign-in failed because:", e);
    });
    

驗證 ID 權杖的完整性

使用 Google API 用戶端程式庫

在實際執行環境中驗證 Google ID 權杖時,建議使用 Java Google API 用戶端程式庫

Java

  import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
  import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
  import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;

  ...

  GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(transport, jsonFactory)
      // Specify the CLIENT_ID of the app that accesses the backend:
      .setAudience(Collections.singletonList(CLIENT_ID))
      // Or, if multiple clients access the backend:
      //.setAudience(Arrays.asList(CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3))
      .build();

  // (Receive idTokenString by HTTPS POST)

  GoogleIdToken idToken = verifier.verify(idTokenString);
  if (idToken != null) {
    Payload payload = idToken.getPayload();

    // Print user identifier
    String userId = payload.getSubject();
    System.out.println("User ID: " + userId);

    // Get profile information from payload
    String email = payload.getEmail();
    boolean emailVerified = Boolean.valueOf(payload.getEmailVerified());
    String name = (String) payload.get("name");
    String pictureUrl = (String) payload.get("picture");
    String locale = (String) payload.get("locale");
    String familyName = (String) payload.get("family_name");
    String givenName = (String) payload.get("given_name");

    // Use or store profile information
    // ...

  } else {
    System.out.println("Invalid ID token.");
  }

GoogleIdTokenVerifier.verify() 方法會驗證 JWT 簽章、aud 憑證附加資訊、iss 憑證附加資訊和 exp 憑證附加資訊。

如果您需要驗證 ID 權杖是否代表 Google Workspace 或 Cloud 機構帳戶,可以檢查 Payload.getHostedDomain() 方法傳回的網域名稱,驗證 hd 權杖。