Android 版关联账号登录

关联账号登录可让已将其 Google 账号关联到您的服务的用户使用通过 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.3.0'
    }
    

修改您的 Android 应用以支持关联账号登录

在关联账号登录流程结束时,ID 令牌会返回到您的应用。在用户登录之前,应验证 ID 令牌的完整性。

以下代码示例详细介绍了检索、验证 ID 令牌以及后续让用户登录的步骤。

  1. 创建一个 Activity 来接收登录 Intent 的结果

    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. 启动登录 Pending Intent

    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 客户端库

使用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 声明以及 exp 声明。

如果您需要验证 ID 令牌是否代表 Google Workspace 或 Cloud 组织账号,您可以通过检查 Payload.getHostedDomain() 方法返回的域名来验证 hd 声明。