目前,许多用户在设置新的 Android 设备时,仍需手动管理自己的凭证。这种手动操作流程不仅繁琐,还常常导致不佳的用户体验。Block Store API 是一个由 Google Play 服务提供支持的库,能够为应用提供一种安全保存用户凭证的方式,既无需处理复杂流程,也能规避保存用户密码带来的安全风险,从而解决这一痛点。
借助 Block Store API,您的应用可以存储数据,以便日后在用户使用新设备时检索这些数据来重新验证用户身份。这一功能可为用户带来更流畅的体验 - 当用户在新设备上首次启动您的应用时,无需再面对登录界面。
使用 Block Store 具有以下优势:
- 为开发者提供加密的凭证存储解决方案。凭证在条件允许时会采用端到端加密。
- 保存令牌,而不是用户名和密码。
- 消除登录流程中的繁琐环节。
- 减轻用户管理复杂密码的负担。
- 由 Google 验证用户身份。
准备工作
为了让您的应用做好准备,请完成以下部分中的步骤。
配置您的应用
在您的项目级 build.gradle 文件中,同时在 buildscript 和 allprojects 两个部分中添加 Google 的 Maven 制品库:
buildscript {
repositories {
google()
mavenCentral()
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
将 Block Store API 的 Google Play 服务依赖项添加到模块的 Gradle build 文件(通常为 app/build.gradle)中:
dependencies {
implementation 'com.google.android.gms:play-services-auth-blockstore:16.4.0'
}
运作方式
Block Store 允许开发者保存和恢复最多 16 个字节数组。这样一来,您就可以保存有关当前用户会话的重要信息,并灵活地以任意方式保存这些信息。这些数据可以进行端到端加密,而支持 Block Store 的基础设施构建于备份和恢复基础设施之上。
本指南将以向 Block Store 保存用户令牌为例,介绍其具体使用方法。以下步骤概述了集成 Block Store 的应用的工作流程:
- 在应用的身份验证流程中或流程结束后的任意时间,您可以将用户的身份验证令牌存储到 Block Store 中,以便日后检索。
- 令牌将存储在本地,也可以备份到云端,并在可能的情况下进行端到端加密。
- 当用户在新设备上启动恢复流程时,数据会随之传输。
- 如果用户在恢复流程中选择恢复您的应用,您的应用便可在新设备上从 Block Store 中检索已保存的令牌。
保存令牌
当用户登录您的应用时,您可以将为该用户生成的身份验证令牌保存到 Block Store。您可以使用唯一键值对来存储此令牌,每个条目的最大大小为 4 KB。如需存储令牌,请对 StoreBytesData.Builder 实例调用 setBytes() 和 setKey(),将用户凭证存储到源设备中。使用 Block Store 保存令牌后,令牌会被加密并存储在设备本地。
以下示例展示了如何将身份验证令牌保存到本地设备:
Java
BlockstoreClient client = Blockstore.getClient(this);
byte[] bytes1 = new byte[] { 1, 2, 3, 4 }; // Store one data block.
String key1 = "com.example.app.key1";
StoreBytesData storeRequest1 = StoreBytesData.Builder()
.setBytes(bytes1)
// Call this method to set the key-value pair the data should be associated with.
.setKeys(Arrays.asList(key1))
.build();
client.storeBytes(storeRequest1)
.addOnSuccessListener(result -> Log.d(TAG, "stored " + result + " bytes"))
.addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));
Kotlin
val client = Blockstore.getClient(this)
val bytes1 = byteArrayOf(1, 2, 3, 4) // Store one data block.
val key1 = "com.example.app.key1"
val storeRequest1 = StoreBytesData.Builder()
.setBytes(bytes1) // Call this method to set the key value with which the data should be associated with.
.setKeys(Arrays.asList(key1))
.build()
client.storeBytes(storeRequest1)
.addOnSuccessListener { result: Int ->
Log.d(TAG,
"Stored $result bytes")
}
.addOnFailureListener { e ->
Log.e(TAG, "Failed to store bytes", e)
}
使用默认令牌
使用 StoreBytes 保存且未指定键的数据会使用默认键 BlockstoreClient.DEFAULT_BYTES_DATA_KEY。
Java
BlockstoreClient client = Blockstore.getClient(this);
// The default key BlockstoreClient.DEFAULT_BYTES_DATA_KEY.
byte[] bytes = new byte[] { 9, 10 };
StoreBytesData storeRequest = StoreBytesData.Builder()
.setBytes(bytes)
.build();
client.storeBytes(storeRequest)
.addOnSuccessListener(result -> Log.d(TAG, "stored " + result + " bytes"))
.addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));
Kotlin
val client = Blockstore.getClient(this);
// the default key BlockstoreClient.DEFAULT_BYTES_DATA_KEY.
val bytes = byteArrayOf(1, 2, 3, 4)
val storeRequest = StoreBytesData.Builder()
.setBytes(bytes)
.build();
client.storeBytes(storeRequest)
.addOnSuccessListener { result: Int ->
Log.d(TAG,
"stored $result bytes")
}
.addOnFailureListener { e ->
Log.e(TAG, "Failed to store bytes", e)
}
检索令牌
之后,当用户在新设备上完成恢复流程时,Google Play 服务会先验证用户身份,再检索您的 Block Store 数据。由于用户在恢复流程中已同意恢复您的应用数据,因此无需额外获取用户授权。当用户打开您的应用时,您可以通过调用 retrieveBytes() 向 Block Store 请求令牌。获取令牌后即可让用户在新设备上保持登录状态。
以下示例展示了如何根据特定键检索多个令牌。
Java
BlockstoreClient client = Blockstore.getClient(this);
// Retrieve data associated with certain keys.
String key1 = "com.example.app.key1";
String key2 = "com.example.app.key2";
String key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY; // Used to retrieve data stored without a key
List<String> requestedKeys = Arrays.asList(key1, key2, key3); // Add keys to array
RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder()
.setKeys(requestedKeys)
.build();
client.retrieveBytes(retrieveRequest)
.addOnSuccessListener(
result -> {
Map<String, BlockstoreData> blockstoreDataMap = result.getBlockstoreDataMap();
for (Map.Entry<String, BlockstoreData> entry : blockstoreDataMap.entrySet()) {
Log.d(TAG, String.format(
"Retrieved bytes %s associated with key %s.",
new String(entry.getValue().getBytes()), entry.getKey()));
}
})
.addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));
Kotlin
val client = Blockstore.getClient(this)
// Retrieve data associated with certain keys.
val key1 = "com.example.app.key1"
val key2 = "com.example.app.key2"
val key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY // Used to retrieve data stored without a key
val requestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array
val retrieveRequest = RetrieveBytesRequest.Builder()
.setKeys(requestedKeys)
.build()
client.retrieveBytes(retrieveRequest)
.addOnSuccessListener { result: RetrieveBytesResponse ->
val blockstoreDataMap =
result.blockstoreDataMap
for ((key, value) in blockstoreDataMap) {
Log.d(ContentValues.TAG, String.format(
"Retrieved bytes %s associated with key %s.",
String(value.bytes), key))
}
}
.addOnFailureListener { e: Exception? ->
Log.e(ContentValues.TAG,
"Failed to store bytes",
e)
}
正在检索所有令牌。
以下示例展示了如何检索保存到 BlockStore 的所有令牌。
Java
BlockstoreClient client = Blockstore.getClient(this)
// Retrieve all data.
RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder()
.setRetrieveAll(true)
.build();
client.retrieveBytes(retrieveRequest)
.addOnSuccessListener(
result -> {
Map<String, BlockstoreData> blockstoreDataMap = result.getBlockstoreDataMap();
for (Map.Entry<String, BlockstoreData> entry : blockstoreDataMap.entrySet()) {
Log.d(TAG, String.format(
"Retrieved bytes %s associated with key %s.",
new String(entry.getValue().getBytes()), entry.getKey()));
}
})
.addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));
Kotlin
val client = Blockstore.getClient(this)
val retrieveRequest = RetrieveBytesRequest.Builder()
.setRetrieveAll(true)
.build()
client.retrieveBytes(retrieveRequest)
.addOnSuccessListener { result: RetrieveBytesResponse ->
val blockstoreDataMap =
result.blockstoreDataMap
for ((key, value) in blockstoreDataMap) {
Log.d(ContentValues.TAG, String.format(
"Retrieved bytes %s associated with key %s.",
String(value.bytes), key))
}
}
.addOnFailureListener { e: Exception? ->
Log.e(ContentValues.TAG,
"Failed to store bytes",
e)
}
以下示例展示了如何检索默认密钥。
Java
BlockStoreClient client = Blockstore.getClient(this);
RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder()
.setKeys(Arrays.asList(BlockstoreClient.DEFAULT_BYTES_DATA_KEY))
.build();
client.retrieveBytes(retrieveRequest);
Kotlin
val client = Blockstore.getClient(this)
val retrieveRequest = RetrieveBytesRequest.Builder()
.setKeys(Arrays.asList(BlockstoreClient.DEFAULT_BYTES_DATA_KEY))
.build()
client.retrieveBytes(retrieveRequest)
正在删除令牌
在以下场景中,可能需要从 BlockStore 中删除令牌:
- 用户执行退出账号流程。
- 令牌已被撤消或无效。
与检索令牌类似,您可以通过设置需要删除的键数组来指定需要删除的令牌。
以下示例演示了如何删除特定键:
Java
BlockstoreClient client = Blockstore.getClient(this);
// Delete data associated with certain keys.
String key1 = "com.example.app.key1";
String key2 = "com.example.app.key2";
String key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY; // Used to delete data stored without key
List<String> requestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array
DeleteBytesRequest deleteRequest = new DeleteBytesRequest.Builder()
.setKeys(requestedKeys)
.build();
client.deleteBytes(deleteRequest)
Kotlin
val client = Blockstore.getClient(this)
// Retrieve data associated with certain keys.
val key1 = "com.example.app.key1"
val key2 = "com.example.app.key2"
val key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY // Used to retrieve data stored without a key
val requestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array
val retrieveRequest = DeleteBytesRequest.Builder()
.setKeys(requestedKeys)
.build()
client.deleteBytes(retrieveRequest)
删除所有令牌
以下示例展示了如何删除保存到 Block Store 的所有令牌:
Java
// Delete all data.
DeleteBytesRequest deleteAllRequest = new DeleteBytesRequest.Builder()
.setDeleteAll(true)
.build();
client.deleteBytes(deleteAllRequest)
.addOnSuccessListener(result -> Log.d(TAG, "Any data found and deleted? " + result));
Kotlin
// Delete all data.
val deleteAllRequest = DeleteBytesRequest.Builder()
.setDeleteAll(true)
.build()
client.deleteBytes(deleteAllRequest)
.addOnSuccessListener { result -> Log.d(TAG, "Any data found and deleted? $result") }
端到端加密
若要使用端到端加密,设备必须搭载 Android 9 或更高版本,并且用户必须为设备设置屏锁(PIN 码、解锁图案或密码)。您可以通过调用 isEndToEndEncryptionAvailable() 来验证设备上是否可以使用加密功能。
以下示例展示了如何验证在云端备份期间是否可以使用加密:
client.isEndToEndEncryptionAvailable()
.addOnSuccessListener { result ->
Log.d(TAG, "Will Block Store cloud backup be end-to-end encrypted? $result")
}
启用云端备份
如需启用云端备份,请将 setShouldBackupToCloud() 方法添加到 StoreBytesData 对象。当 setShouldBackupToCloud() 设置为 true 时,Block Store 会定期将存储的字节备份到云端。
以下示例展示了如何仅在云端备份支持端到端加密时启用云端备份:
val client = Blockstore.getClient(this)
val storeBytesDataBuilder = StoreBytesData.Builder()
.setBytes(/* BYTE_ARRAY */)
client.isEndToEndEncryptionAvailable()
.addOnSuccessListener { isE2EEAvailable ->
if (isE2EEAvailable) {
storeBytesDataBuilder.setShouldBackupToCloud(true)
Log.d(TAG, "E2EE is available, enable backing up bytes to the cloud.")
client.storeBytes(storeBytesDataBuilder.build())
.addOnSuccessListener { result ->
Log.d(TAG, "stored: ${result.getBytesStored()}")
}.addOnFailureListener { e ->
Log.e(TAG, "Failed to store bytes", e)
}
} else {
Log.d(TAG, "E2EE is not available, only store bytes for D2D restore.")
}
}
如何测试
在开发期间,请使用以下方法来测试恢复流程。
在同一设备上卸载并重新安装
如果用户启用了备份服务(可在设置 > Google > 备份中查看),则在应用卸载并重新安装后,Block Store 数据仍会保留。
您可以按照以下步骤进行测试:
- 将 Block Store API 集成到测试应用中。
- 使用测试应用调用 Block Store API 来存储数据。
- 卸载测试应用,然后在同一设备上重新安装该应用。
- 使用测试应用调用 Block Store API 以检索您的数据。
- 验证检索到的字节与卸载前存储的字节相同。
设备到设备
在大多数情况下,这需要将目标设备恢复出厂设置。然后,您可以进入 Android 无线恢复流程或 Google 数据线恢复流程(适用于受支持的设备)。
云端恢复
- 将 Block Store API 集成到您的测试应用中。测试应用需要提交到 Play 商店。
- 在源设备上,使用测试应用调用 Block Store API 来存储数据,并将
shouldBackUpToCloud设置为true。 - 在搭载 Android 8.0(API 级别 26)或更高版本的设备上,您可以手动触发 Block Store 云端备份:前往设置 > Google > 备份,然后点击“立即备份”按钮。
- 如需验证 Block Store 云端备份是否成功,您可以:
- 备份完成后,搜索带有
[CloudSyncBackupTaskService]标记的日志行。 - 您应该会看到如下所示的内容:
..., [CloudSyncBackupTaskService] sync result: SUCCESS,..., uploaded size: [size] bytes ...
- 备份完成后,搜索带有
- Block Store 云备份完成后,会有 5 分钟的“冷却”期。在这 5 分钟内,点击“立即备份”按钮不会触发其他 Block Store 云端备份。
- 如需验证 Block Store 云端备份是否成功,您可以:
- 将目标设备恢复出厂设置,然后完成云端恢复流程。在恢复流程中,选择恢复您的测试应用。如需详细了解云端恢复流程,请参阅支持的云端恢复流程。
- 在目标设备上,使用测试应用调用 Block Store API 来检索数据。
- 验证检索到的字节与存储在源设备中的字节相同。
设备要求
查看以下针对参与 Block Store 操作的设备的要求。
端到端加密
- 搭载 Android 9 (API 29) 或更高版本的设备支持端到端加密。
- 设备必须已设置屏锁(支持 PIN 码、解锁图案或密码),才能启用端到端加密并正确加密用户的数据。
设备到设备恢复流程
设备到设备恢复需要您拥有源设备和目标设备。数据将在这两台设备之间传输。
源设备必须搭载 Android 6 (API 23) 或更高版本才支持备份功能。
目标设备必须搭载 Android 9 (API 29) 或更高版本才支持恢复功能。
如需详细了解设备到设备恢复流程,请参阅支持的设备到设备恢复流程。
云端备份和恢复流程
云端备份和恢复需要源设备和目标设备。
源设备必须搭载 Android 6 (API 23) 或更高版本才支持备份功能。
目标设备的支持情况因供应商而异。Pixel 设备从 Android 9 (API 29) 开始支持此功能,而所有其他设备必须搭载 Android 12 (API 31) 或更高版本。