块店

许多用户在设置新的 Android 设备时仍会管理自己的凭据。这种手动流程可能很难处理,并且通常会导致用户体验不佳。Block Store API 是一个由 Google Play 服务提供支持的库,旨在通过为应用提供一种保存用户凭据的方式来解决此问题,而不会带来与保存用户密码相关的复杂性或安全风险。

借助 Block Store API,您的应用可以存储数据,以便日后检索这些数据,在新设备上重新对用户进行身份验证。这样有助于为用户提供更加流畅的体验,因为他们在新设备上首次启动您的应用时无需看到登录屏幕。

使用 Block Store 的好处包括:

  • 面向开发者的加密凭据存储解决方案。凭据会尽可能进行端到端加密。
  • 保存令牌,而不是用户名和密码。
  • 消除登录流程中的摩擦。
  • 让用户免去管理复杂密码的负担。
  • Google 会验证用户的身份。

准备工作

为了让您的应用做好准备,请完成以下部分中的步骤。

配置您的应用

在您的项目级 build.gradle 文件中,同时在 buildscriptallprojects 两个部分中添加 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 的应用的工作原理:

  1. 在应用的身份验证流程中或之后的任何时间,您都可以将用户的身份验证令牌存储到分块存储区,以供日后检索。
  2. 令牌将存储在本地,也可以备份到云端,并尽可能进行端到端加密。
  3. 当用户在新设备上发起恢复流程时,系统会传输数据。
  4. 如果用户在恢复流程中恢复您的应用,您的应用便可从新设备上的 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

List 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 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)

删除所有令牌

以下示例展示了如何删除当前保存到 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 key BlockstoreClient.DEFAULT_BYTES_DATA_KEY can be used
in the RetrieveBytesRequest 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 数据会在应用卸载/重新安装后保留。

您可以按照以下步骤进行测试:

  1. 将 Block Store API 集成到您的测试应用中。
  2. 使用测试应用调用 Block Store API 来存储数据。
  3. 卸载测试应用,然后在同一设备上重新安装应用。
  4. 使用测试应用调用 Block Store API 以检索数据。
  5. 验证检索到的字节是否与卸载前存储的字节相同。

设备到设备

在大多数情况下,这需要将目标设备恢复出厂设置。然后,您可以进入 Android 无线恢复流程Google 数据线恢复流程(适用于受支持的设备)。

云端恢复

  1. 将 Block Store API 集成到您的测试应用中。测试应用需要提交到 Play 商店。
  2. 在源设备上,使用测试应用调用 Block Store API 来存储数据,并将 shouldBackUpToCloud 设置为 true
  3. 对于 O 及更高版本的设备,您可以手动触发 Block Store 云端备份:依次前往设置 > Google > 备份,然后点击“立即备份”按钮。
    1. 如需验证 Block Store 云端备份是否成功,您可以执行以下操作:
      1. 备份完成后,搜索标记为“CloudSyncBpTkSvc”的日志行。
      2. 您应该会看到如下所示的行:“......, CloudSyncBpTkSvc: sync result: SUCCESS, ..., uploaded size: XXX bytes ...”
    2. 进行 Block Store 云端备份后,系统会有一个 5 分钟的“冷却”期。在 5 分钟内,点击“立即备份”按钮不会触发另一次 Block Store 云端备份。
  4. 将目标设备恢复出厂设置,然后完成云端恢复流程。在恢复流程中选择恢复测试应用。如需详细了解云端恢复流程,请参阅支持的云端恢复流程
  5. 在目标设备上,使用测试应用调用 Block Store API 以检索数据。
  6. 验证检索到的字节是否与存储在源设备中的字节相同。

设备要求

端到端加密

  • 搭载 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) 或更高版本。