块存储

许多用户在设置新的 Android 设备时,仍然需要手动管理自己的凭据。这种手动过程可能会很麻烦,并且通常会导致糟糕的用户体验。Block Store API 是一个由 Google Play 服务提供支持的库,旨在通过提供一种方式,让应用能够在保存用户凭据时避免保存用户密码带来的复杂性和安全风险,从而解决这个问题。

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

使用块存储的好处包括:

  • 开发者加密凭据存储解决方案。凭据在可能的情况下进行端到端加密。
  • 保存令牌而非用户名和密码。
  • 消除登录流程中的障碍。
  • 让用户摆脱管理复杂密码的负担。
  • Google 验证用户的身份。

准备工作

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

配置您的应用

在项目级 build.gradle 文件中,在 buildscriptallprojects 部分中都包含 Google 的 Maven 代码库

buildscript {
  repositories {
    google()
    mavenCentral()
  }
}

allprojects {
  repositories {
    google()
    mavenCentral()
  }
}

将 Block Store API 的 Google Play 服务依赖项添加到模块的 Gradle 构建文件(通常是 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 的应用如何工作:

  1. 在您的应用身份验证流程中,或在此后的任何时间,您可以将用户的身份验证令牌存储到 Block Store 以便日后检索。
  2. 令牌将存储在本地,在可能的情况下也可以端到端加密备份到云端。
  3. 当用户在新设备上启动恢复流程时,数据会进行传输。
  4. 如果在恢复流程中用户恢复了您的应用,您的应用便可以从新设备上的 Block Store 检索已保存的令牌。

保存令牌

当用户登录您的应用时,您可以将为该用户生成的身份验证令牌保存到 Block Store。您可以使用一个独特的键值对来存储此令牌,每个条目最大为 4kb。要存储令牌,请在 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

以下示例展示了如何检索默认键。

Java

端到端加密

为了启用端到端加密,设备必须运行 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 时,块存储会定期将存储的字节备份到云端。

以下示例展示了如何仅在云备份进行端到端加密时启用云备份:

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