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

透過已連結帳戶的登入機制,如果使用者已將自己的 Google 帳戶與您的服務連結,他們就能啟用One Tap Sign In 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 檔案中,請同時在 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.2.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 用戶端程式庫

如要在實際工作環境中驗證 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 憑證附加資訊。

呼叫 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 代管的網域。