封鎖商店

許多使用者在設定新的 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 版本檔案,通常為 app/build.gradle

dependencies {
  implementation 'com.google.android.gms:play-services-auth-blockstore:16.2.0'
}

運作方式

Block Store 可讓開發人員儲存及還原上限為 16 位元組陣列。如此一來,您就可以儲存目前使用者工作階段的重要資訊,並視需要彈性儲存這些資訊。這些資料可進行端對端加密處理,支援 Block Store 的基礎架構是以「備份與還原」基礎架構為基礎。

本指南會說明將使用者權杖儲存至「封鎖商店」的應用實例。以下步驟概述利用 Block Store 的應用程式運作方式:

  1. 在應用程式驗證流程期間,或之後隨時都可以將使用者的驗證權杖儲存至 Block Store,以便日後擷取。
  2. 權杖會儲存在本機中,也可以備份至雲端 (在可能的情況下進行端對端加密)。
  3. 使用者在新裝置上啟動還原流程時,系統會轉移資料。
  4. 如果使用者在還原流程期間還原應用程式,應用程式便可在新裝置上從 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 blockstoreDataMap = result.getBlockstoreDataMap();
          for (Map.Entry 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 blockstoreDataMap = result.getBlockstoreDataMap();
          for (Map.Entry 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()
client.deleteBytes(deleteAllRequest)
  .addOnSuccessListener { result: Boolean ->
    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」>「備份」查看),在解除安裝/重新安裝應用程式的過程中,系統會保留封鎖商店資料。

測試步驟如下:

  1. 將 BlockStore API 整合至測試應用程式。
  2. 使用測試應用程式來叫用 BlockStore API,以儲存資料。
  3. 解除安裝測試應用程式,然後在相同裝置上重新安裝應用程式。
  4. 使用測試應用程式叫用 BlockStore API 以擷取資料。
  5. 確認擷取的位元組與解除安裝前儲存的位元組數相同。

裝置對裝置

在大多數情況下,這麼做需要將目標裝置恢復原廠設定。接著,您可以進入 Android 無線還原流程Google 傳輸線還原流程 (適用於支援的裝置)。

雲端還原

  1. 將 Blockstore API 整合至測試應用程式。測試應用程式必須提交至 Play 商店。
  2. 在來源裝置上,使用測試應用程式叫用 Blockstore API 來儲存資料,並將 shouldBackUpToCloud 設為 true。
  3. 如果是 O 及以上裝置,您可以手動觸發 Block Store 雲端備份:依序前往「設定」>「Google」>「備份」,然後按一下「立即備份」按鈕。
    1. 如要確認 Block Store 雲端備份是否已順利完成,你可以採取下列動作:
      1. 備份完成後,搜尋含有「CloudSyncBpTkSvc」標記的記錄行。
      2. 您應該會看到類似下列的內容:「......, CloudSyncBpTkSvc: sync result: SUCCESS, ..., upload size: XXXBytes ...」
    2. Block Store 雲端備份完成後,會有 5 分鐘的「等待期」。在這 5 分鐘內點選「立即備份」按鈕,並不會觸發另一個 Block Store 雲端備份。
  4. 將目標裝置恢復原廠設定,並完成雲端還原流程。選取即可在還原流程中還原測試應用程式。如要進一步瞭解雲端還原流程,請參閱支援的雲端還原流程
  5. 在目標裝置上,使用測試應用程式叫用 Blockstore 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) 以上版本。