许多用户在设置新的 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'
}
工作原理
借助分块存储区,开发者最多可以保存和恢复 16 个字节数组。这样,您就可以保存与当前用户会话相关的重要信息,并灵活地以您喜欢的方式保存这些信息。这些数据可以进行端到端加密,支持 Block Store 的基础架构是基于备份和恢复基础架构构建的。
本指南将介绍将用户的令牌保存到分块存储区的用例。以下步骤概述了利用 Block Store 的应用的工作原理:
- 在应用的身份验证流程中或之后的任何时间,您都可以将用户的身份验证令牌存储到分块存储区,以供日后检索。
- 令牌将存储在本地,也可以备份到云端,并尽可能进行端到端加密。
- 当用户在新设备上发起恢复流程时,系统会传输数据。
- 如果用户在恢复流程中恢复您的应用,您的应用便可从新设备上的 Block Store 检索已保存的令牌。
保存令牌
当用户登录您的应用时,您可以将为该用户生成的身份验证令牌保存到分块存储区。您可以使用唯一的键值对值存储此令牌,每个条目的大小不得超过 4kb。如需存储令牌,请对 StoreBytesData.Builder
的实例调用 setBytes()
和 setKey()
,以将用户的凭据存储到来源设备。使用块存储区保存令牌后,系统会对令牌进行加密,并将其存储在设备本地。
以下示例展示了如何将身份验证令牌保存到本地设备:
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 服务会先验证用户,然后检索您的区块存储数据。用户已在恢复流程中同意恢复您的应用数据,因此无需征得额外的同意。当用户打开您的应用时,您可以通过调用 retrieveBytes()
从区块存储区请求令牌。然后,系统可以使用检索到的令牌让用户在新设备上保持登录状态。
以下示例展示了如何根据特定键检索多个令牌。
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 ListrequestedKeys = 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 ListrequestedKeys = 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)
删除所有令牌
以下示例展示了如何删除当前保存到 BlockStore 的所有令牌:
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
val deleteAllRequest = DeleteBytesRequest.Builder() .setDeleteAll(true) .build() retrieve bytes, the keyBlockstoreClient.DEFAULT_BYTES_DATA_KEY
can be used in theRetrieveBytesRequest
instance in order to get your saved data
The following example shows how to retrieve the default key.
Java
End-to-end encryption
In order for end-to-end encryption to be made available, the device must be
running Android 9 or higher, and the user must have set a screen lock
(PIN, pattern, or password) for their device. You can verify if encryption will
be available on the device by calling isEndToEndEncryptionAvailable()
.
The following sample shows how to verify if encryption will be available during cloud backup:
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
。 - 对于 O 及更高版本的设备,您可以手动触发 Block Store 云端备份:依次前往设置 > Google > 备份,然后点击“立即备份”按钮。
- 如需验证 Block Store 云端备份是否成功,您可以执行以下操作:
- 备份完成后,搜索标记为“CloudSyncBpTkSvc”的日志行。
- 您应该会看到如下所示的行:“......, CloudSyncBpTkSvc: sync result: SUCCESS, ..., uploaded size: XXX bytes ...”
- 进行 Block Store 云端备份后,系统会有一个 5 分钟的“冷却”期。在 5 分钟内,点击“立即备份”按钮不会触发另一次 Block Store 云端备份。
- 如需验证 Block Store 云端备份是否成功,您可以执行以下操作:
- 将目标设备恢复出厂设置,然后完成云端恢复流程。在恢复流程中选择恢复测试应用。如需详细了解云端恢复流程,请参阅支持的云端恢复流程。
- 在目标设备上,使用测试应用调用 Block Store API 以检索数据。
- 验证检索到的字节是否与存储在源设备中的字节相同。
设备要求
端到端加密
- 搭载 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) 或更高版本。