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 services」

  2. 如果這些套件的狀態尚未安裝,請兩者同時選取,然後按一下「Install Packages」(安裝套件)

設定應用程式

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

    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.0.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 正確簽署 ID 權杖。使用 Google 的公開金鑰 (提供 JWKPEM 格式) 驗證權杖的簽名。這些金鑰會定期輪替;請檢查回應中的 Cache-Control 標頭,判斷何時應再次擷取這些金鑰。
  • ID 權杖中的 aud 值等於應用程式的其中一個用戶端 ID。必須進行這項檢查,以免核發至惡意應用程式的 ID 權杖,藉此存取應用程式後端伺服器上的相同使用者相關資料。
  • ID 權杖中的 iss 值等於 accounts.google.comhttps://accounts.google.com
  • ID 權杖的到期時間 (exp) 尚未超過。
  • 如果您需要驗證 ID 權杖代表 Google Workspace 或 Cloud 機構帳戶,可以查看 hd 憑證附加資訊,這就表示使用者的託管網域。將資源的存取權限制為僅限特定網域的成員存取時,必須使用這個屬性。缺少這項聲明,代表帳戶不屬於 Google 代管網域。

比起自行編寫程式碼來執行這些驗證步驟,我們強烈建議您使用適用於您平台的 Google API 用戶端程式庫,或一般用途的 JWT 程式庫。如要進行開發和偵錯,您可以呼叫我們的 tokeninfo 驗證端點。

使用 Google API 用戶端程式庫

我們建議您使用 Java Google API 用戶端程式庫在實際工作環境中驗證 Google ID 權杖。

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 憑證附加資訊及 JWT 憑證附加資訊exp

如果您需要驗證 ID 權杖是否代表 Google Workspace 或 Cloud 機構帳戶,請查看 Payload.getHostedDomain() 方法傳回的網域名稱來驗證 hd 憑證附加資訊。

呼叫 Tokeninfo 端點

如要驗證偵錯所需的 ID 權杖簽章,最簡單的方法是使用 tokeninfo 端點。呼叫這個端點需要額外的網路要求,這類要求會為您執行大部分的驗證作業,同時在您自己的程式碼中測試適當的驗證與酬載擷取作業。不適用於實際工作環境程式碼中,因為要求可能會受到限製或發生間歇性錯誤。

如要使用 tokeninfo 端點驗證 ID 權杖,請向端點發出 HTTPS POST 或 GET 要求,並在 id_token 參數中傳遞 ID 權杖。 舉例來說,如要驗證權杖「XYZ123」,請提出下列 GET 要求:

https://oauth2.googleapis.com/tokeninfo?id_token=XYZ123

如果權杖已正確簽署,且 issexp 憑證附加資訊含有預期值,您會收到「HTTP 200」回應,其中主體包含 JSON 格式的 ID 權杖憑證附加資訊。以下是回應範例:

{
 // These six fields are included in all Google ID Tokens.
 "iss": "https://accounts.google.com",
 "sub": "110169484474386276334",
 "azp": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "aud": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
 "iat": "1433978353",
 "exp": "1433981953",

 // These seven fields are only included when the user has granted the "profile" and
 // "email" OAuth scopes to the application.
 "email": "testuser@gmail.com",
 "email_verified": "true",
 "name" : "Test User",
 "picture": "https://lh4.googleusercontent.com/-kYgzyAWpZzJ/ABCDEFGHI/AAAJKLMNOP/tIXL9Ir44LE/s99-c/photo.jpg",
 "given_name": "Test",
 "family_name": "User",
 "locale": "en"
}

如果您需要驗證 ID 權杖是否代表 Google Workspace 帳戶,可以查看 hd 憑證附加資訊,這會指出使用者的代管網域。將資源的存取權限制為僅限特定網域的成員存取時,必須使用這個屬性。若缺少這項聲明,代表帳戶不屬於 Google Workspace 代管的網域。