谷歌登录又失败了

2,447 阅读9分钟

运营:我们的App怎么谷歌登录失败了呢?

我:我看看呢?

于是把项目跑起来,一切正常。好吧,看看我怎么打你脸!

我:我这里正常呢,你看看我的录屏?

运营:咦?让测试看看呢?

测试:我试了一下,我手上的几台设备登录都失败了!

我:?

c6a4466d34d0c27846dc862b221e9d3.jpg

一、签名疑云之Android App Bundle

于是我再次运行项目检查,确实登录成功。难道是网络问题?于是所有设备使用同一个翻墙工具,使用同一个网络节点,结果还是不行?

将所有设备汇总到我这里,依次安装releae下的apk,所有设备检查均正常。

于是尝试打包aab,测试将aab转为apk后看看效果。

bundletool.jar build-apks --connected-device --bundle=app-release.aab --output=app-googlePlay-release.apks

bundletool.jar install-apks --apks=app-googlePlay-release.apks

安装后通过运行时获取签名信息,检查一下:

public void checkAppSignature(Context context) {
        try {
            // 获取包管理器
            PackageManager packageManager = context.getPackageManager();
            // 获取应用的信息,包括签名信息
            PackageInfo packageInfo = packageManager.getPackageInfo(
                context.getPackageName(),
                PackageManager.GET_SIGNATURES);
            // 遍历签名数组,通常一个应用只有一个签名
            for (android.content.pm.Signature signature : packageInfo.signatures) {
                byte[] signatureBytes = signature.toByteArray();
                Log.d(TAG, "签名的字节数组: " + bytesToHexString(signatureBytes));
                
                // 使用SHA-256进行哈希计算
                String sha256Hash = hashSignature(signatureBytes, "SHA-256");
                RtEvent.report("login_google", "login_google", null, "SHA-256 : " + sha256Hash);

                // 使用MD5进行哈希计算
                String md5Hash = hashSignature(signatureBytes, "MD5");
                RtEvent.report("login_google", "login_google", null, "MD5 : " + md5Hash);

                // 使用SHA1进行哈希计算
                String sha1Hash = hashSignature(signatureBytes, "SHA1");
                RtEvent.report("login_google", "login_google", null, "SHA1 : " + sha1Hash);
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "未找到包", e);
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "未找到算法", e);
        }
    }

    private String hashSignature(byte[] signatureBytes, String algorithm) throws NoSuchAlgorithmException {
        MessageDigest md = MessageDigest.getInstance(algorithm);
        md.update(signatureBytes);
        byte[] digest = md.digest();
        return bytesToHexString(digest);
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString().toLowerCase();
    }

通过日志观察,发现这个签名和appbuild.gradle指定的签名不一致。 我再次查看了build.gradle下的签名配置

buildTypes {
    debug {
        signingConfig signingConfigs.release
        minifyEnabled false//代码混淆
        shrinkResources false//资源压缩
        //代码压缩
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    release {
        firebaseCrashlytics {
            mappingFileUploadEnabled false
        }
        signingConfig signingConfigs.release
        minifyEnabled true
        shrinkResources true
        //代码压缩
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

签名确实都是指定的signingConfigs.release,但是打包输出的为什么不是我指定的签名呢? 经过进一步分析,发现运行时获取的签名为C:\Users\admin\.android\debug.keystore的签名。

原来是解析aab的过程问题,官方文档有说明:

本部分将介绍如何使用 bundletool 在本地测试 app bundle。

当 bundletool 从 app bundle 生成 APK 后,它会将生成的 APK 纳入到一个名为“APK set archive”的容器中,该容器以 .apks 作为文件扩展名。**如需从 app bundle 为应用支持的所有设备配置生成一组 APK,请使用 bundletool build-apks 命令,如下所示:

bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks

如果要将这些 APK 部署到设备,您还需要添加应用的签名信息,如以下命令所示。如果您未指定签名信息,bundletool 会尝试使用调试密钥为 APK 签名。

bundletool build-apks --bundle=/MyApp/my_app.aab --output=/MyApp/my_app.apks
--ks=/MyApp/keystore.jks
--ks-pass=file:/MyApp/keystore.pwd
--ks-key-alias=MyKeyAlias
--key-pass=file:/MyApp/key.pwd

下表更详细地介绍了使用bundletool build-apks命令时可以设置的各种标志和选项:

1.bundletool build-apks命令的选项

标志说明
--bundle=path(必需)指定您使用 Android Studio 构建的 app bundle 的路径。如需了解详情,请参阅构建您的项目。
--output=path(必需)指定输出 .apks 文件的名称,该文件中包含了应用的所有 APK 工件。如需在设备上测试此文件中的工件,请按照有关如何将 APK 部署到已连接设备的部分中的步骤操作。
--overwrite使用您通过 --output 选项指定的路径覆盖任何现有的输出文件。如果您不添加此标志,而输出文件已存在,您会遇到构建错误。
--aapt2=path指定 AAPT2 的自定义路径。默认情况下,bundletool 包含自己的 AAPT2 版本。
--ks=path(可选)指定用于为 APK 签名的部署密钥库的路径。如果您不添加此标志,bundletool 会尝试使用调试签名密钥为您的 APK 签名。
--ks-pass=pass:password

--ks-pass=file:/path/to/file
指定密钥库密码。如果您指定纯文本格式的密码,请使用 pass: 限定该密码。如果您要传递包含该密码的文件的路径,请使用 file: 限定该路径。如果您使用 --ks 标志指定密钥库,而未指定 --ks-pass,那么 bundletool 会提示您从命令行输入密码。
--ks-key-alias=alias指定要使用的签名密钥的别名。
--key-pass=pass:password

--key-pass=file:/path/to/file
指定签名密钥的密码。如果您指定纯文本格式的密码,请使用 pass: 限定该密码。如果您要传递包含该密码的文件的路径,请使用 file: 限定该路径。如果此密码与密钥库自身的密码相同,您可以省略此标志。
--connected-device指示 bundletool 针对已连接设备的配置构建 APK。如果您不添加此标记,bundletool 会为您的应用支持的所有设备配置生成 APK。
--device-id=serial-number如果您有多个已连接的设备,请使用此标志指定要部署应用的设备的序列号。
--device-spec=spec_json提供 .json 文件的路径,该文件指定了您要针对其生成 APK 的设备配置。如需了解详情,请参阅有关如何生成并使用设备规范 JSON 文件的部分。
--mode=universal将模式设置为 universal。如果您希望 bundletool 构建一个包含应用的所有代码和资源的 APK,以使该 APK 与应用支持的所有设备配置兼容,请使用此选项。
注意:bundletool 仅包含功能模块,这些模块在通用 APK 中的对应清单中指定 <dist:fusing dist:include="true"/>。如需了解详情,请参阅功能模块清单。
请注意,这些 APK 要比针对特定设备配置优化过的 APK 更大。但是,这些 APK 更便于与内部测试人员共享,例如想在多种设备配置上测试应用的测试人员。
--local-testing启用 app bundle 进行本地测试。在本地测试时,由于无需上传到 Google Play 服务器,因此能够实现快速的迭代测试周期。
如需查看如何使用 --local-testing 标志测试模块安装的示例,请参阅 在本地测试模块的安装情况。

如需查看如何使用--local-testing标志测试模块安装的示例,请参阅 在本地测试模块的安装情况。

bundletool更多信息点击查看

到此处终于弄明白了为什么aab签名不一致的问题!

二、签名疑云之GooglePlay

但是还有一个问题,当aab上传到GooglePlay后,GooglePlay会再次签名,此时用户安装的apk签名还是和我本地的签名不一致呢!

原来GooglePlay签名有两种

  • 上传密钥证书
  • 应用签名密钥证书

上传密钥证书,是我们本地签名时使用的证书,用于GooglePlay验证安装包是否来自同一个签名。 应用签名密钥证书,是GooglePlay对我们上传的安装包再次签名使用的证书。

image.png 两个签名有什么问题呢?比如我们通过firebase创建Android应用时需要提供SHA1,而创建时我们已经提供了本地签名的密钥证书,即上面的上传密钥证书。这里又蹦出一个应用签名证书怎么办呢?

image.png

其实也简单,点击添加指纹,将GooglePlay返回的应用签名密钥证书——SHA1或者SHA-256选其一或者全部添加进去,然后重新下载google-service.json添加到本地即可,这样就可以支持多个签名了!

三、client_id我该如何抉择?

此时下载到本地的google-service.json有一个叫oauth_client的节点,内部包含了三个client_id,然后还有一个other_platform_oauth_client的节点,它也包含了一个client_id

{
  "project_info": {
    "project_number": "596*******03",
    "firebase_url": "https://yowin-****-****-default-rtdb.firebaseio.com",
    "project_id": "yowin-****-*****",
    "storage_bucket": "yowin-****-*****.appspot.com"
  },
  "client": [
    {
      "client_info": {
        "mobilesdk_app_id": "1:496*******02:android:8af52*********9bd926bb",
        "android_client_info": {
          "package_name": "com.********.pl****"
        }
      },
      "oauth_client": [
        {
          "client_id": "596110360803-4hj51jodhit702por3bedavbd6i71crc.apps.googleusercontent.com",
          "client_type": 1,
          "android_info": {
            "package_name": "com.********.pl****",
            "certificate_hash": "df29796796ae95f50a2ff866fb0c21f0ebcd24dd"
          }
        },
        {
          "client_id": "596110360803-ijmmi6fh75n09rbpl0ji6hln6d6at8iq.apps.googleusercontent.com",
          "client_type": 1,
          "android_info": {
            "package_name": "com.********.pl****",
            "certificate_hash": "54a553e02503e3d458c58f50c786155f61132b9d"
          }
        },
        {
          "client_id": "596110360803-bba1jgq4b28eo71fjvdj94ftloshqdib.apps.googleusercontent.com",
          "client_type": 3
        }
      ],
      "api_key": [
        {
          "current_key": "AIzaSyDVecvBriSDYvrpunGjGCrYrc"
        }
      ],
      "services": {
        "appinvite_service": {
          "other_platform_oauth_client": [
            {
              "client_id": "596110360803-bba1jgq4b28eo71fjvdj94ftloshqdib.apps.googleusercontent.com",
              "client_type": 3
            }
          ]
        }
      }
    }
  ],
  "configuration_version": "1"
}

在谷歌登录的时候,我开始根据签名来选择对应的client_id,当本地使用本地签名的APK,则使用client_type1client_id,当上传到谷歌的aab,则使用client_type2client_id,遗憾的是又失败了!

public void login() {
    
    if (mOptions==null){
        mOptions = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
            .requestIdToken("596110360803-4hj51jodhit702por3bedavbd6i71crc.apps.googleusercontent.com")
                .requestEmail()
                .build();
    }
    if (mClient==null){
        mClient = GoogleSignIn.getClient(AppLifeMng.Shared().getTopActivity(), mOptions);
    }

    Intent signInIntent = mClient.getSignInIntent();
    AppLifeMng.Shared().getTopActivity().startActivityForResult(signInIntent, ResultCode.Login_Google);
}

d833c895d143ad4b0df177328e025aafa50f0660.gif

此刻的我是懵逼的,签名没有问题,包名没有问题,设备时间也没有问题(是的,设备时间和本地时间不一致也有这个问题),但是我就是登录失败!

但是我使用使用client_type3client_id,又正常了。这是为什么呢?

024f78f0f736afc39e090f97bf19ebc4b745123e.gif

好吧,目前总算是解决问题了,虽然我也不知道怎么解决的!

这需要您获取服务器的 Web客户端 ID。有关如何获取的详细信息,请参阅此处

Android 开发者博客:在服务器和 Google 服务之间使用凭据 (googleblog.com)

小朋友才做选择,成年人全都要!如果不依赖这个client_id是否可以呢?答案当然是可以!

来看看firebaseUI吧!

  • 创建一个 ActivityResultLauncher,它可为 FirebaseUI Activity 结果合约注册回调:
// See: https://developer.android.com/training/basics/intents/result
private final ActivityResultLauncher<Intent> signInLauncher = registerForActivityResult(
        new FirebaseAuthUIActivityResultContract(),
        new ActivityResultCallback<FirebaseAuthUIAuthenticationResult>() {
            @Override
            public void onActivityResult(FirebaseAuthUIAuthenticationResult result) {
                onSignInResult(result);
            }
        }
);
  • 如需启动FirebaseUI登录流程,请使用您偏好的登录方法创建登录Intent
// Choose authentication providers
List<AuthUI.IdpConfig> providers = Arrays.asList(
        new AuthUI.IdpConfig.GoogleBuilder().build());

// Create and launch sign-in intent
Intent signInIntent = AuthUI.getInstance()
        .createSignInIntentBuilder()
        .setAvailableProviders(providers)
        .build();
signInLauncher.launch(signInIntent);
  • 登录流程完成后,您会在 onSignInResult 中收到结果:
private void onSignInResult(FirebaseAuthUIAuthenticationResult result) {
    IdpResponse response = result.getIdpResponse();
    if (result.getResultCode() == RESULT_OK) {
        // Successfully signed in
        FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
        // ...
    } else {
        // Sign in failed. If response is null the user canceled the
        // sign-in flow using the back button. Otherwise check
        // response.getError().getErrorCode() and handle the error.
        // ...
    }
}

关于FirebaseUI的更多信息请参阅使用 FirebaseUI 轻松向 Android 应用添加登录机制

四、总结

  • bundletool.jar build-apks --connected-device --bundle=app-release.aab --output=app-googlePlay-release.apks可以将aab转为apk,但是会将签名信息设置为本地默认的签名信息(C:\Users\admin\.android\debug.keystore)
  • 上传GooglePlay的应用,应注意应用签名密钥证书,并在对应平台更新密钥证书
  • 谷歌登录失败的话,更新google-service.json,并设置Web应用类型的客户端ID

五、补充

使用Google帐号进行身份验证(Android)里面有详细说明ID的选择说明:

您必须将“服务器”客户端ID传递给 setGoogleIdTokenRequestOptions 方法。如需查找OAuth 2.0客户端ID,请执行以下操作:

  1. GCP控制台中打开“凭据”页面
  2. Web应用类型的客户端ID就是您的后端服务器的OAuth 2.0客户端ID