保险库仓库
使用 VaultTemplate 以及将响应映射到 Java 类的方式,可以实现基本的数据操作,如读取、写入和删除。Vault 存储库在 Vault 的基础上应用了 Spring Data 的存储库概念。Vault 存储库暴露了基本的 CRUD 功能,并支持通过谓词约束标识符属性、分页和排序来派生查询。Vault 存储库使用键/值(key/value)机密引擎功能来持久化和查询数据。从 2.4 版本开始,Spring Vault 还可以使用键/值版本 2 机密引擎,实际的机密引擎版本在运行时被检测到。
在版本化的键/值存储引擎中,删除操作使用 DELETE
操作。通过 CrudRepository.delete(…)
不会销毁密钥。
有关 Spring Data 仓库的更多信息,请参阅 Spring Data Commons 参考文档。参考文档将为你介绍 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
实体的基本 Repository 接口
interface CredentialsRepository extends CrudRepository<Credentials, String> {
}
由于我们的仓库扩展了 CrudRepository
,它提供了基本的 CRUD 和查询方法。Vault 仓库需要 Spring Data 组件。请确保在你的类路径中包含 spring-data-commons
和 spring-data-keyvalue
构件。
实现这一点的最简单方法是设置依赖管理,并将构件添加到你的 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.2</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
}
将
Credentials
的属性存储在 Vault Hash 中,使用键模式keyspace/id
,在本例中为credentials/heisenberg
,存储在键值对秘密引擎中。使用提供的 id 检索存储在
keyspace/id
处的对象。计算由
Credentials
上的@Secret
定义的 credentials 键空间内可用的实体总数。从 Vault 中移除给定对象的键。
对象到 Vault JSON 映射
Vault 仓库使用 JSON 作为交换格式在 Vault 中存储对象。JSON 和实体之间的对象映射由 VaultConverter
完成。该转换器读取和写入包含来自 VaultResponse
的正文的 SecretDocument
。VaultResponse
从 Vault 中读取,并由 Jackson 将正文反序列化为 String
和 Object
的 Map
。默认的 VaultConverter
实现读取包含嵌套值、List
和 Map
对象的 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"
}
}
_class
属性包含在根级别以及任何嵌套的接口或抽象类型中。简单属性值通过路径映射。
复杂类型的属性被映射为嵌套对象。
@Id
属性必须映射为 String
类型。
表 1. 默认映射规则
类型 | 示例 | 映射值 |
---|---|---|
简单类型 (例如 String) | String firstname = "Walter"; | "firstname": "Walter" |
复杂类型 (例如 Address) | Address address = 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. 查询方法支持的关键字
关键字 | 示例 |
---|---|
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 过滤之后应用,并且所有结果中的密钥都从 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
,并且秘密不会被更新。
版本属性必须是数字属性,例如 int
或 long
,并且在更新密钥时映射到 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>
根据其 Id
sample-credentials
获取一个 secret。根据其 Id
sample-credentials
获取第二个 secret 实例。更新 secret 并让 Vault 增加版本号。
更新使用先前版本的第二个实例。由于在此期间 Vault 中的版本号已增加,操作将失败并抛出
OptimisticLockingFailureException
。
在删除版本化的密钥时,按 Id 删除会删除最新的密钥。按实体删除会删除指定版本的密钥。
访问版本化密钥
Key/Value 版本 2 的 secrets engine 维护了可以通过在 Vault 仓库接口声明中实现 RevisionRepository 来访问的 secrets 版本。Revision 仓库定义了查找方法,以获取特定标识符的修订版本。标识符必须是 String
类型。
示例 10. 实现 RevisionRepository
interface RevisionCredentialsRepository extends CrudRepository<Credentials, String>,
RevisionRepository<Credentials, String, Integer> 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));