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));