关联帐号登录 (Android)

关联的账号登录功能可让已将 Google 账号与您的服务相关联的用户一键登录。这可以改善 因为他们只需点击一下即可登录,无需再次进入, 用户名和密码进行登录。这还可以降低用户在您的服务中创建重复账号的几率。

“关联的账号登录”是以下账号的“一键登录”流程的一部分: Android。这意味着,如果您的应用 已启用一键式功能。

在本文档中,您将了解如何修改 Android 应用以支持 关联的账号登录。

工作原理

  1. 您可以在一键式登录流程中选择显示关联的账号。
  2. 如果用户已登录 Google 账号,并已将自己的 Google 账号与 就会返回一个 ID 令牌,用于关联 。
  3. 系统会向用户显示一键登录提示,并提供登录您 关联账号使用 Google 服务。
  4. 如果用户选择继续使用关联的账号,则用户的 ID 令牌 返回给您的应用您需要将此令牌与发送到 服务器识别已登录的用户。

设置

设置您的开发环境

在开发主机上获取最新的 Google Play 服务:

  1. 打开 Android SDK 管理器
  1. SDK Tools 下,找到 Google Play 服务

  2. 如果这些软件包的状态未为“已安装”,请同时选中它们并点击 安装软件包

配置您的应用

  1. 在项目级 build.gradle 文件中,添加 Google 的 Maven 制品库 在buildscriptallprojects部分中均有提供。

    buildscript {
        repositories {
            google()
        }
    }
    
    allprojects {
        repositories {
            google()
        }
    }
    
  2. 将“与 Google 关联”API 的依赖项添加到模块的应用级 Gradle 文件(通常为 app/build.gradle):

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

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

在“已关联的账号登录”流程结束时,系统会将一个 ID 令牌返回给您的 应用。在用户登录之前,应验证 ID 令牌的完整性。

以下代码示例详细介绍了检索 验证 ID 令牌 确保用户登录

  1. 创建一个 activity 以接收 Sign-In 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. 启动登录待处理 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 令牌的完整性

To verify that the token is valid, ensure that the following criteria are satisfied:

  • The ID token is properly signed by Google. Use Google's public keys (available in JWK or PEM format) to verify the token's signature. These keys are regularly rotated; examine the Cache-Control header in the response to determine when you should retrieve them again.
  • The value of aud in the ID token is equal to one of your app's client IDs. This check is necessary to prevent ID tokens issued to a malicious app being used to access data about the same user on your app's backend server.
  • The value of iss in the ID token is equal to accounts.google.com or https://accounts.google.com.
  • The expiry time (exp) of the ID token has not passed.
  • If you need to validate that the ID token represents a Google Workspace or Cloud organization account, you can check the hd claim, which indicates the hosted domain of the user. This must be used when restricting access to a resource to only members of certain domains. The absence of this claim indicates that the account does not belong to a Google hosted domain.

Using the email, email_verified and hd fields, you can determine if Google hosts and is authoritative for an email address. In the cases where Google is authoritative, the user is known to be the legitimate account owner, and you may skip password or other challenge methods.

Cases where Google is authoritative:

  • email has a @gmail.com suffix, this is a Gmail account.
  • email_verified is true and hd is set, this is a G Suite account.

Users may register for Google Accounts without using Gmail or G Suite. When email does not contain a @gmail.com suffix and hd is absent, Google is not authoritative and password or other challenge methods are recommended to verify the user. email_verified can also be true as Google initially verified the user when the Google account was created, however ownership of the third party email account may have since changed.

Rather than writing your own code to perform these verification steps, we strongly recommend using a Google API client library for your platform, or a general-purpose JWT library. For development and debugging, you can call our tokeninfo validation endpoint.

使用 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 组织账号,您可以通过检查域名来验证 hd 所有权声明 由 Payload.getHostedDomain() 方法返回。

调用 tokeninfo 端点

调试验证 ID 令牌签名的一种简单方法是 使用 tokeninfo 端点。调用此端点涉及 这个额外的网络请求会为您完成大部分的验证工作, 验证和载荷提取。不适合在生产环境中使用 因为请求可能会受到限制或出现间歇性错误。

如需使用 tokeninfo 端点验证 ID 令牌,请创建 HTTPS POST 或 GET 请求发送到端点,并在 id_token 参数。 例如,要验证令牌“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 托管网域。