Vault Repositories

使用 VaultTemplate 並將響應對映到 Java 類,可以進行讀、寫和刪除等基本資料操作。Vault Repositories 在 Vault 之上應用了 Spring Data 的 Repository 概念。Vault Repository 暴露了基本的 CRUD 功能,並支援帶有約束識別符號屬性的查詢派生、分頁和排序。Vault Repositories 使用鍵/值 Secret Engine 功能來持久化和查詢資料。從 2.4 版本開始,Spring Vault 還可以使用鍵/值版本 2 Secret Engine,實際的 Secret Engine 版本在執行時發現。

在版本化的鍵/值 Secret Engine 中,刪除操作使用 DELETE 命令。Secrets 不會透過 CrudRepository.delete(…) 被銷燬。
Vault Repositories 透過 Vault 的 sys/internal/ui/mounts/… 端點確定掛載路徑。請確保您的策略允許訪問該路徑,否則將無法使用 Repository 抽象。
有關 Spring Data Repositories 的更多資訊,請參閱 Spring Data Commons 參考文件。該參考文件將向您介紹 Spring Data Repositories。

用法

為了訪問儲存在 Vault 中的域實體,您可以利用 Repository 支援,這極大地簡化了實現過程。

示例 1. 憑據實體示例
@Secret
class Credentials {

  @Id String id;
  String password;
  String socialSecurityNumber;
  Address address;
}

這裡我們有一個非常簡單的域物件。請注意,它有一個名為 id 的屬性,帶有 org.springframework.data.annotation.Id 註解,並且在其型別上有一個 @Secret 註解。這兩個註解負責建立用於將物件作為 JSON 持久化到 Vault 中的實際鍵。

帶有 @Id 註解的屬性以及名稱為 id 的屬性都被視為識別符號屬性。帶有註解的屬性優先於其他屬性。

下一步是宣告一個使用該域物件的 Repository 介面。

示例 2. Credentials 實體的基本 Repository 介面
interface CredentialsRepository extends CrudRepository<Credentials, String> {

}

由於我們的 Repository 擴充套件了 CrudRepository,它提供了基本的 CRUD 和查詢方法。Vault Repositories 需要 Spring Data 元件。請確保在您的類路徑中包含 spring-data-commonsspring-data-keyvalue artifact。

最簡單的方法是設定依賴管理,並將 artifact 新增到您的 pom.xml 中。

然後將以下內容新增到 pom.xml 的 dependencies 部分。

示例 3. 使用 Spring Data BOM
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-bom</artifactId>
      <version>2023.1.9</version>
      <scope>import</scope>
      <type>pom</type>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>

  <!-- other dependency elements omitted -->

  <dependency>
    <groupId>org.springframework.vault</groupId>
    <artifactId>spring-vault-core</artifactId>
    <version>3.1.3</version>
  </dependency>

  <dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-keyvalue</artifactId>
    <!-- Version inherited from the BOM -->
  </dependency>

</dependencies>

我們中間需要一個東西來連線這些事物,那就是相應的 Spring 配置。

示例 4. Vault Repositories 的 JavaConfig
@Configuration
@EnableVaultRepositories
class ApplicationConfig {

  @Bean
  VaultTemplate vaultTemplate() {
    return new VaultTemplate(…);
  }
}

有了上面的設定,我們就可以繼續將 CredentialsRepository 注入到我們的元件中。

示例 5. 訪問 人員 實體
@Autowired CredentialsRepository repo;

void basicCrudOperations() {

  Credentials creds = new Credentials("heisenberg", "327215", "AAA-GG-SSSS");
  rand.setAddress(new Address("308 Negra Arroyo Lane", "Albuquerque", "New Mexico", "87104"));

  repo.save(creds);                                        (1)

  repo.findOne(creds.getId());                             (2)

  repo.count();                                            (3)

  repo.delete(creds);                                      (4)
}
1 Credentials 的屬性儲存在 Vault Hash 中,鍵模式為 keyspace/id,在本例中為 credentials/heisenberg,儲存在鍵-值 secret secrets engine 中。
2 使用提供的 ID 來檢索儲存在 keyspace/id 的物件。
3 計算由 Credentials 上的 @Secret 定義的 credentials 鍵空間中可用實體的總數。
4 從 Vault 中移除給定物件的鍵。

物件到 Vault JSON 對映

Vault Repositories 使用 JSON 作為交換格式將物件儲存在 Vault 中。JSON 與實體之間的物件對映由 VaultConverter 完成。轉換器讀取和寫入包含來自 VaultResponse 的主體的 SecretDocumentVaultResponse 從 Vault 讀取,其主體由 Jackson 反序列化為一個 StringObjectMap。預設的 VaultConverter 實現讀取帶有巢狀值、ListMap 物件的 Map,並將它們轉換為實體,反之亦然。

考慮到前幾節中的 Credentials 型別,預設對映如下:

{
  "_class": "org.example.Credentials",                 (1)
  "password": "327215",                                (2)
  "socialSecurityNumber": "AAA-GG-SSSS",
  "address": {                                         (3)
    "street": "308 Negra Arroyo Lane",
    "city": "Albuquerque",
    "state": "New Mexico",
    "zip": "87104"
  }
}
1 _class 屬性包含在根級別以及任何巢狀介面或抽象型別上。
2 簡單屬性值透過路徑對映。
3 複雜型別的屬性對映為巢狀物件。
@Id 屬性必須對映到 String
表 1. 預設對映規則
型別 示例 對映值

簡單型別
(例如 String)

String firstname = "Walter";

"firstname": "Walter"

複雜型別
(例如 Address)

Address adress = new Address("308 Negra Arroyo Lane");

"address": { "street": "308 Negra Arroyo Lane" }

List
簡單型別列表

List<String> nicknames = asList("walt", "heisenberg");

"nicknames": ["walt", "heisenberg"]

Map
簡單型別列表

Map<String, Integer> atts = asMap("age", 51)

"atts" : {"age" : 51}

List
複雜型別列表

List<Address> addresses = asList(new Address("308…

"address": [{ "street": "308 Negra Arroyo Lane" }, …]

您可以透過在 VaultCustomConversions 中註冊 Converter 來定製對映行為。這些轉換器可以負責從/到 LocalDate 等型別以及 SecretDocument 的轉換,其中前者適合轉換簡單屬性,而後者適合將複雜型別轉換為其 JSON 表示。第二種選項提供了對結果 SecretDocument 的完全控制。將物件寫入 Vault 將刪除原有內容並重新建立整個條目,因此未對映的資料將會丟失。

查詢和查詢方法

查詢方法允許從方法名稱自動派生簡單查詢。Vault 沒有查詢引擎,但需要直接訪問 HTTP 上下文路徑。Vault 查詢方法將 Vault 的 API 功能轉換為查詢。查詢方法執行時會列出上下文路徑下的子項,對 Id 應用過濾,可選擇使用偏移量/限制來限制 Id 流,並在獲取結果後應用排序。

示例 6. Repository 查詢方法示例
interface CredentialsRepository extends CrudRepository<Credentials, String> {

  List<Credentials> findByIdStartsWith(String prefix);
}
Vault Repositories 的查詢方法僅支援對 @Id 屬性進行謂詞的查詢。

以下是 Vault 支援的關鍵字概覽。

表 2. 查詢方法支援的關鍵字
關鍵字 示例

After, GreaterThan

findByIdGreaterThan(String id)

GreaterThanEqual

findByIdGreaterThanEqual(String id)

Before, LessThan

findByIdLessThan(String id)

LessThanEqual

findByIdLessThanEqual(String id)

Between

findByIdBetween(String from, String to)

In

findByIdIn(Collection ids)

NotIn

findByIdNotIn(Collection ids)

Like, StartingWith, EndingWith

findByIdLike(String id)

NotLike, IsNotLike

findByIdNotLike(String id)

Containing

findByFirstnameContaining(String id)

NotContaining

findByFirstnameNotContaining(String name)

Regex

findByIdRegex(String id)

(無關鍵字)

findById(String name)

Not

findByIdNot(String id)

And

findByLastnameAndFirstname

Or

findByLastnameOrFirstname

Is,Equals

findByFirstname,findByFirstnameIs,findByFirstnameEquals

Top,First

findFirst10ByFirstname,findTop5ByFirstname

排序和分頁

查詢方法透過在記憶體中選擇一個子列表(偏移量/限制)來支援排序和分頁,該子列表包含從 Vault 上下文路徑檢索到的 Id。排序不像查詢方法謂詞那樣僅限於特定欄位。未分頁的排序在 Id 過濾後應用,並且所有結果 Secrets 都從 Vault 獲取。這樣,查詢方法只會獲取作為結果一部分返回的結果。

使用分頁和排序需要在過濾 Id 之前獲取 Secret,這會影響效能。排序和分頁保證即使 Vault 返回的 Id 自然順序發生變化,也能返回相同的結果。因此,首先從 Vault 獲取所有 Id,然後應用排序,之後進行過濾和偏移量/限制。

示例 7. 分頁和排序 Repository
interface CredentialsRepository extends PagingAndSortingRepository<Credentials, String> {

  List<Credentials> findTop10ByIdStartsWithOrderBySocialSecurityNumberDesc(String prefix);

  List<Credentials> findByIdStarts(String prefix, Pageable pageRequest);
}

樂觀鎖

Vault 的鍵/值 Secret Engine 版本 2 可以維護版本化的 Secret。Spring Vault 透過域模型中帶有 @Version 註解的版本屬性來支援版本控制。使用樂觀鎖確保更新僅應用於版本匹配的 Secret。因此,版本屬性的實際值透過 cas 屬性新增到更新請求中。如果在此期間另一個操作修改了 Secret,則會丟擲 OptimisticLockingFailureException 並且 Secret 不會被更新。

版本屬性必須是數字屬性,例如 intlong,並在更新 Secret 時對映到 cas 屬性。

示例 8. 版本化實體示例
@Secret
class VersionedCredentials {

  @Id String id;
  @Version int version;
  String password;
  String socialSecurityNumber;
  Address address;
}

以下示例展示了這些特性。

示例 9. 版本化實體示例
VersionedCredentialsRepository repo = …;

VersionedCredentials credentials = repo.findById("sample-credentials").get();    (1)

VersionedCredentials concurrent = repo.findById("sample-credentials").get();     (2)

credentials.setPassword("something-else");

repos.save(credentials);                                                         (3)


concurrent.setPassword("concurrent change");

repos.save(concurrent); // throws OptimisticLockingFailureException              (4)
1 透過其 Id sample-credentials 獲取一個 Secret。
2 透過其 Id sample-credentials 獲取該 Secret 的第二個例項。
3 更新 Secret 並讓 Vault 遞增版本。
4 更新使用前一個版本的第二個例項。由於在此期間 Vault 中的版本已遞增,該操作將失敗並丟擲 OptimisticLockingFailureException
刪除版本化的 Secret 時,按 Id 刪除會刪除最新的 Secret。按實體刪除會刪除指定版本處的 Secret。

訪問版本化的 Secret

鍵/值版本 2 Secret Engine 維護 Secret 的版本,可以透過在您的 Vault Repository 介面宣告中實現 RevisionRepository 來訪問這些版本。Revision Repositories 定義了查詢特定識別符號版本的方法。識別符號必須是 String 型別。

示例 10. 實現 RevisionRepository
interface RevisionCredentialsRepository extends CrudRepository<Credentials, String>,
                                        RevisionRepository<Credentials, String, Integer> (1)
{

}
1 第一個型別引數(Credentials)表示實體型別,第二個型別引數(String)表示 Id 屬性的型別,最後一個型別引數(Integer)是修訂號的型別。Vault 僅支援 String 識別符號和 Integer 修訂號。

用法

現在您可以使用 RevisionRepository 中的方法來查詢實體的修訂版本,如下例所示:

示例 11. 使用 RevisionRepository
RevisionCredentialsRepository repo = …;

Revisions<Integer, Credentials> revisions = repo.findRevisions("my-secret-id");

Page<Revision<Integer, Credentials>> firstPageOfRevisions = repo.findRevisions("my-secret-id", Pageable.ofSize(4));