Vault 存储库

使用 VaultTemplate 和映射到 Java 类的响应允许进行基本的读、写和删除等数据操作。Vault 存储库将 Spring Data 的存储库概念应用于 Vault 之上。Vault 存储库公开基本的 CRUD 功能,并支持使用约束标识符属性、分页和排序的谓词进行查询派生。Vault 存储库使用键/值机密引擎功能来持久化和查询数据。从 2.4 版开始,Spring Vault 还可以使用键/值版本 2 机密引擎,实际机密引擎版本在运行时发现。

版本化键/值机密引擎中的删除使用 DELETE 操作。机密不会通过 CrudRepository.delete(…) 销毁。
Spring Data Commons 参考文档 中详细了解 Spring Data 存储库。参考文档将向你介绍 Spring Data 存储库。

用法

要访问存储在 Vault 中的域实体,你可以利用存储库支持,这可以极大地简化实现。

示例 1. 样本凭证实体
@Secret
class Credentials {

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

这里有一个非常简单的域对象。请注意,它有一个名为 id 的属性,该属性使用 org.springframework.data.annotation.Id 进行了注释,并且其类型上有一个 @Secret 注释。这两个注释负责创建用于将对象作为 JSON 持久化到 Vault 中的实际密钥。

@Id 注释的属性以及命名为 id 的属性被视为标识符属性。优先使用带注释的属性。

下一步是声明一个使用领域对象的数据存储库接口。

示例 2. Credentials 实体的基本数据存储库接口
interface CredentialsRepository extends CrudRepository<Credentials, String> {

}

由于我们的数据存储库扩展了 CrudRepository,因此它提供了基本的 CRUD 和查询方法。Vault 数据存储库需要 Spring Data 组件。确保在类路径中包含 spring-data-commonsspring-data-keyvalue 工件。

实现此目标最简单的方法是设置依赖项管理并在 pom.xml 中添加工件

然后将以下内容添加到 pom.xml 依赖项部分。

示例 3. 使用 Spring Data BOM
<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-bom</artifactId>
      <version>2023.1.2</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.1</version>
  </dependency>

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

</dependencies>

我们需要的东西是将所有内容粘合在一起的相应 Spring 配置。

示例 4. Vault 数据存储库的 JavaConfig
@Configuration
@EnableVaultRepositories
class ApplicationConfig {

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

根据以上设置,我们可以继续将 CredentialsRepository 注入到我们的组件中。

示例 5. 访问 Person 实体
@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 在 Vault Hash 中存储 Credentials 的属性,密钥模式为 keyspace/id,在本例中为 credentials/heisenberg,在键值秘密 secrets 引擎中。
2 使用提供的 id 检索存储在 keyspace/id 中的对象。
3 计算 Credentials 上由 @Secret 定义的键空间 credentials 中可用的实体总数。
4 从 Vault 中移除给定对象的密钥。

对象到 Vault JSON 映射

Vault 数据存储库使用 JSON 作为交换格式将对象存储在 Vault 中。JSON 和实体之间的对象映射由 VaultConverter 完成。转换器读取和写入 SecretDocument,其中包含来自 VaultResponse 的正文。从 Vault 中读取 VaultResponse,并且正文由 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 adress = new Address("308 Negra Arroyo Lane");

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

列表
简单类型

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

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

映射
简单类型

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

"atts" : {"age" : 51}

列表
复杂类型

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. 样例存储库查询方法
interface CredentialsRepository extends CrudRepository<Credentials, String> {

  List<Credentials> findByIdStartsWith(String prefix);
}
Vault 存储库的查询方法仅支持对 @Id 属性使用谓词的查询。

以下是 Vault 支持的关键字概述。

表 2. 查询方法支持的关键字
关键字 示例

AfterGreaterThan

findByIdGreaterThan(String id)

GreaterThanEqual

findByIdGreaterThanEqual(String id)

BeforeLessThan

findByIdLessThan(String id)

LessThanEqual

findByIdLessThanEqual(String id)

Between

findByIdBetween(String from, String to)

In

findByIdIn(Collection ids)

NotIn

findByIdNotIn(Collection ids)

LikeStartingWithEndingWith

findByIdLike(String id)

NotLikeIsNotLike

findByIdNotLike(String id)

包含

findByFirstnameContaining(String id)

不包含

findByFirstnameNotContaining(String name)

正则表达式

findByIdRegex(String id)

(无关键字)

findById(String name)

Not

findByIdNot(String id)

And

findByLastnameAndFirstname

Or

findByLastnameOrFirstname

Is、Equals

findByFirstnamefindByFirstnameIsfindByFirstnameEquals

Top、First

findFirst10ByFirstnamefindTop5ByFirstname

排序和分页

查询方法支持通过从 Vault 上下文路径中检索的子列表(偏移量/限制)ID 在内存中进行排序和分页。与查询方法谓词不同,排序并不局限于特定字段。未分页的排序在 ID 过滤后应用,并且所有结果机密都从 Vault 中获取。通过这种方式,查询方法仅获取作为结果的一部分返回的结果。

使用分页和排序需要在过滤 ID 之前获取机密,这会影响性能。排序和分页保证返回相同的结果,即使 Vault 返回的 ID 的自然顺序发生变化。因此,首先从 Vault 中获取所有 ID,然后应用排序,最后进行过滤和偏移/限制。

示例 7. 分页和排序存储库
interface CredentialsRepository extends PagingAndSortingRepository<Credentials, String> {

  List<Credentials> findTop10ByIdStartsWithOrderBySocialSecurityNumberDesc(String prefix);

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

乐观锁

Vault key/value 机密引擎版本 2 可以维护版本化机密。Spring Vault 通过使用 @Version 注释的域模型中的版本属性支持版本控制。使用乐观锁可确保仅将更新应用于版本匹配的机密。因此,通过 cas 属性将版本属性的实际值添加到更新请求中。如果在此期间另一个操作更改了机密,则会抛出 OptimisticLockingFailureException,并且机密不会更新。

版本属性必须是数字属性,例如 intlong,并且在更新机密时映射到 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 获取机密。
2 通过其 ID sample-credentials 获取机密的第二个实例。
3 更新机密并让 Vault 增加版本。
4 更新使用先前版本的第二个实例。该操作将失败,并出现 OptimisticLockingFailureException,因为 Vault 中的版本已同时增加。
删除版本化机密时,按 ID 删除将删除最新的机密。按实体删除将删除指定版本处的机密。

访问版本化机密

Key/Value 版本 2 机密引擎维护机密的版本,可以通过在 Vault 存储库接口声明中实现 RevisionRepository 来访问这些版本。修订存储库定义查找方法以获取特定标识符的修订。标识符必须是 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));