运营:我们的App怎么谷歌登录失败了呢?
我:我看看呢?
于是把项目跑起来,一切正常。好吧,看看我怎么打你脸!
我:我这里正常呢,你看看我的录屏?
运营:咦?让测试看看呢?
测试:我试了一下,我手上的几台设备登录都失败了!
我:?
一、签名疑云之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();
}
通过日志观察,发现这个签名和app下build.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标志测试模块安装的示例,请参阅 在本地测试模块的安装情况。
到此处终于弄明白了为什么aab签名不一致的问题!
二、签名疑云之GooglePlay
但是还有一个问题,当aab上传到GooglePlay后,GooglePlay会再次签名,此时用户安装的apk签名还是和我本地的签名不一致呢!
原来GooglePlay签名有两种
- 上传密钥证书
- 应用签名密钥证书
上传密钥证书,是我们本地签名时使用的证书,用于GooglePlay验证安装包是否来自同一个签名。
应用签名密钥证书,是GooglePlay对我们上传的安装包再次签名使用的证书。
两个签名有什么问题呢?比如我们通过
firebase创建Android应用时需要提供SHA1,而创建时我们已经提供了本地签名的密钥证书,即上面的上传密钥证书。这里又蹦出一个应用签名证书怎么办呢?
其实也简单,点击添加指纹,将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_type为1的client_id,当上传到谷歌的aab,则使用client_type为2的client_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);
}
此刻的我是懵逼的,签名没有问题,包名没有问题,设备时间也没有问题(是的,设备时间和本地时间不一致也有这个问题),但是我就是登录失败!
但是我使用使用client_type为3的client_id,又正常了。这是为什么呢?
好吧,目前总算是解决问题了,虽然我也不知道怎么解决的!
这需要您获取服务器的 Web客户端
ID。有关如何获取的详细信息,请参阅此处
小朋友才做选择,成年人全都要!如果不依赖这个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,请执行以下操作:
- 在
GCP控制台中打开“凭据”页面。- Web应用类型的客户端
ID就是您的后端服务器的OAuth 2.0客户端ID。