块店

许多用户在设置新的 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'
}

工作原理

Block Store 允许开发者保存和恢复最多 16 个字节数组。这样一来,您就可以保存有关当前用户会话的重要信息,并灵活地以任意方式保存这些信息。这些数据可以进行端到端加密,支持 Block Store 的基础架构构建在备份和恢复基础架构之上。

本指南将介绍将用户令牌保存到 Block Store 的用例。以下步骤概述了利用块存储区的应用的工作方式:

  1. 在应用的身份验证流程中或之后随时,您都可以将用户的身份验证令牌存储到 Block Store 中,以便日后检索。
  2. 令牌将存储在本地,也可以备份到云端,并在可能的情况下进行端到端加密。
  3. 当用户在新设备上启动恢复流程时,系统会转移数据。
  4. 如果用户在恢复流程中恢复您的应用,您的应用随后便可从新设备上的 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 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)或更高版本。