Docs 菜单
Docs 主页
/ /

使用Java中的 Spring Data MongoDB实现 CSFLE

本指南向您展示如何将MongoDB 客户端字段级加密(CSFLE) 与 Spring Data MongoDB集成。

注意

如果您以前未使用过 CSFLE,请参阅MongoDB Server手册中的 CSFLE 教程指南。

如果您以前未使用过 Spring Data MongoDB ,请参阅 Spring Data MongoDB文档。

要查看本指南的源代码,请参阅 GitHub 上的 mongodb-java-spring-boot-csfle存储库。

您还可以通过运行以下命令从其 GitHub存储库检索源代码:

git clone git@github.com:mongodb-developer/mongodb-java-spring-boot-csfle.git

在使用本指南之前,请执行以下步骤:

  1. 安装Java17

  2. 安装MongoDB自动加密共享库 v7.0.2 或更高版本

  3. 查看 README.md 以了解有关如何运行示例代码的信息。

您还需要访问权限运行MongoDB v 或更高版本的MongoDB7.0.2 集群。

此示例为生产环境提供模板化代码,并包括以下功能:

  • 多个加密集合

  • 自动生成JSON schema

  • 服务器端JSON schema

  • 数据加密密钥 (DEK) 和加密集合的独立集群

  • 自动生成或检索 DEK

  • Spring 表达式语言 (SpEL) 评估扩展

  • 自动实现的存储库

  • OpenAPI 3.0.1 文档

该模板遵循 SOLID 原则,以提高代码可读性和重用性。要学习;了解有关 SOLID 的更多信息,请参阅 Wikipedia 上的 SOLID。

下图显示了创建支持 CSFLE 且可自动加密和解密字段的 MongoClient 所需的组件。图中的箭头显示了组件之间的依赖和关系。

项目高级图表

图中的箭头显示了组件之间的依赖和关系。

应用程序与MongoDB建立连接后,会使用三层架构来公开REST API并管理与MongoDB 数据库的通信,如下图所示:

三层架构

本部分介绍如何创建密钥保管集合及其唯一索引。

如果要从新的MongoDB 集群开始,则必须首先在 keyAltNames字段上创建密钥保管库集合及其唯一索引。

KeyVaultAndDekSetup 类通过使用与MongoDB的标准连接来创建密钥保管库集合及其唯一索引。然后,它创建加密每个加密集合中的文档所需的 DEK。

以下代码显示了 KeyVaultAndDekSetup 类:

@Component
public class KeyVaultAndDekSetup {
private static final Logger LOGGER = LoggerFactory.getLogger(KeyVaultAndDekSetup.class);
private final KeyVaultService keyVaultService;
private final DataEncryptionKeyService dataEncryptionKeyService;
@Value("${spring.data.mongodb.vault.uri}")
private String CONNECTION_STR;
public KeyVaultAndDekSetup(KeyVaultService keyVaultService, DataEncryptionKeyService dataEncryptionKeyService) {
this.keyVaultService = keyVaultService;
this.dataEncryptionKeyService = dataEncryptionKeyService;
}
@PostConstruct
public void postConstruct() {
LOGGER.info("=> Start Encryption Setup.");
LOGGER.debug("=> MongoDB Connection String: {}", CONNECTION_STR);
MongoClientSettings mcs = MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(CONNECTION_STR))
.build();
try (MongoClient client = MongoClients.create(mcs)) {
LOGGER.info("=> Created the MongoClient instance for the encryption setup.");
LOGGER.info("=> Creating the encryption key vault collection.");
keyVaultService.setupKeyVaultCollection(client);
LOGGER.info("=> Creating the Data Encryption Keys.");
EncryptedCollectionsConfiguration.encryptedEntities.forEach(dataEncryptionKeyService::createOrRetrieveDEK);
LOGGER.info("=> Encryption Setup completed.");
} catch (Exception e) {
LOGGER.error("=> Encryption Setup failed: {}", e.getMessage(), e);
}
}
}

重要

在生产中,您可以在 keyAltNames字段上手动创建密钥保管库集合及其唯一索引。创建它们后,您可以删除此代码。

此代码在 try-with-resources区块中使用标准(未启用 CSFLE)和临时 MongoClient 连接,在MongoDB 集群中创建集合和索引。

KeyVaultServiceImpl 类创建密钥保管集合和 keyAltNames唯一索引。

以下代码显示了 KeyVaultServiceImpl 类:

@Service
public class KeyVaultServiceImpl implements KeyVaultService {
private static final Logger LOGGER = LoggerFactory.getLogger(KeyVaultServiceImpl.class);
private static final String INDEX_NAME = "uniqueKeyAltNames";
@Value("${mongodb.key.vault.db}")
private String KEY_VAULT_DB;
@Value("${mongodb.key.vault.coll}")
private String KEY_VAULT_COLL;
public void setupKeyVaultCollection(MongoClient mongoClient) {
LOGGER.info("=> Setup the key vault collection {}.{}", KEY_VAULT_DB, KEY_VAULT_COLL);
MongoDatabase db = mongoClient.getDatabase(KEY_VAULT_DB);
MongoCollection<Document> vault = db.getCollection(KEY_VAULT_COLL);
boolean vaultExists = doesCollectionExist(db, KEY_VAULT_COLL);
if (vaultExists) {
LOGGER.info("=> Vault collection already exists.");
if (!doesIndexExist(vault)) {
LOGGER.info("=> Unique index created on the keyAltNames");
createKeyVaultIndex(vault);
}
} else {
LOGGER.info("=> Creating a new vault collection & index on keyAltNames.");
createKeyVaultIndex(vault);
}
}
private void createKeyVaultIndex(MongoCollection<Document> vault) {
Bson keyAltNamesExists = exists("keyAltNames");
IndexOptions indexOpts = new IndexOptions().name(INDEX_NAME)
.partialFilterExpression(keyAltNamesExists)
.unique(true);
vault.createIndex(new BsonDocument("keyAltNames", new BsonInt32(1)), indexOpts);
}
private boolean doesCollectionExist(MongoDatabase db, String coll) {
return db.listCollectionNames().into(new ArrayList<>()).stream().anyMatch(c -> c.equals(coll));
}
private boolean doesIndexExist(MongoCollection<Document> coll) {
return coll.listIndexes()
.into(new ArrayList<>())
.stream()
.map(i -> i.get("name"))
.anyMatch(n -> n.equals(INDEX_NAME));
}
}

创建密钥保管库集合和索引后,可以关闭标准MongoDB连接。

本部分介绍如何使用 ClientEncryption 连接创建 DEK。

MongoDBKeyVaultClientConfiguration 类创建一个 ClientEncryption bean,DataEncryptionKeyService 使用该 bean 创建 DEK。

以下代码显示了 MongoDBKeyVaultClientConfiguration 类:

@Configuration
public class MongoDBKeyVaultClientConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(MongoDBKeyVaultClientConfiguration.class);
private final KmsService kmsService;
@Value("${spring.data.mongodb.vault.uri}")
private String CONNECTION_STR;
@Value("${mongodb.key.vault.db}")
private String KEY_VAULT_DB;
@Value("${mongodb.key.vault.coll}")
private String KEY_VAULT_COLL;
private MongoNamespace KEY_VAULT_NS;
public MongoDBKeyVaultClientConfiguration(KmsService kmsService) {
this.kmsService = kmsService;
}
@PostConstruct
public void postConstructor() {
this.KEY_VAULT_NS = new MongoNamespace(KEY_VAULT_DB, KEY_VAULT_COLL);
}
/**
* MongoDB Encryption Client that can manage Data Encryption Keys (DEKs).
*
* @return ClientEncryption MongoDB connection that can create or delete DEKs.
*/
@Bean
public ClientEncryption clientEncryption() {
LOGGER.info("=> Creating the MongoDB Key Vault Client.");
MongoClientSettings mcs = MongoClientSettings.builder()
.applyConnectionString(new ConnectionString(CONNECTION_STR))
.build();
ClientEncryptionSettings ces = ClientEncryptionSettings.builder()
.keyVaultMongoClientSettings(mcs)
.keyVaultNamespace(KEY_VAULT_NS.getFullName())
.kmsProviders(kmsService.getKmsProviders())
.build();
return ClientEncryptions.create(ces);
}
}

您可以使用密钥管理系统ClientEncryption (KMS) 创建 bean,并使用它来生成 DEK。每个加密集合都需要一个 DEK。

DataEncryptionKeyServiceImpl 类创建并存储 DEK。当您评估实体中的 SpEL 表达式以创建JSON schema时,您必须检索DEK。

以下代码显示了 DataEncryptionKeyServiceImpl 类:

@Service
public class DataEncryptionKeyServiceImpl implements DataEncryptionKeyService {
private static final Logger LOGGER = LoggerFactory.getLogger(DataEncryptionKeyServiceImpl.class);
private final ClientEncryption clientEncryption;
private final Map<String, String> dataEncryptionKeysB64 = new HashMap<>();
@Value("${mongodb.kms.provider}")
private String KMS_PROVIDER;
public DataEncryptionKeyServiceImpl(ClientEncryption clientEncryption) {
this.clientEncryption = clientEncryption;
}
public Map<String, String> getDataEncryptionKeysB64() {
LOGGER.info("=> Getting Data Encryption Keys Base64 Map.");
LOGGER.info("=> Keys in DEK Map: {}", dataEncryptionKeysB64.entrySet());
return dataEncryptionKeysB64;
}
public String createOrRetrieveDEK(EncryptedEntity encryptedEntity) {
Base64.Encoder b64Encoder = Base64.getEncoder();
String dekName = encryptedEntity.getDekName();
BsonDocument dek = clientEncryption.getKeyByAltName(dekName);
BsonBinary dataKeyId;
if (dek == null) {
LOGGER.info("=> Creating Data Encryption Key: {}", dekName);
DataKeyOptions dko = new DataKeyOptions().keyAltNames(of(dekName));
dataKeyId = clientEncryption.createDataKey(KMS_PROVIDER, dko);
LOGGER.debug("=> DEK ID: {}", dataKeyId);
} else {
LOGGER.info("=> Existing Data Encryption Key: {}", dekName);
dataKeyId = dek.get("_id").asBinary();
LOGGER.debug("=> DEK ID: {}", dataKeyId);
}
String dek64 = b64Encoder.encodeToString(dataKeyId.getData());
LOGGER.debug("=> Base64 DEK ID: {}", dek64);
LOGGER.info("=> Adding Data Encryption Key to the Map with key: {}",
encryptedEntity.getEntityClass().getSimpleName());
dataEncryptionKeysB64.put(encryptedEntity.getEntityClass().getSimpleName(), dek64);
return dek64;
}
}

注意

由于 DEK 存储在映射中,因此您以后无需为JSON schemas 再次检索DEK。

Spring Data MongoDB使用以 POJO 为中心的模型来实现存储库并将文档映射到MongoDB集合。

以下代码显示了 PersonEntity 类:

@Document("persons")
@Encrypted(keyId = "#{mongocrypt.keyId(#target)}")
public class PersonEntity {
@Id
private ObjectId id;
private String firstName;
private String lastName;
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic")
private String ssn;
@Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Random")
private String bloodType;
public PersonEntity() {
}
public PersonEntity(ObjectId id, String firstName, String lastName, String ssn, String bloodType) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.ssn = ssn;
this.bloodType = bloodType;
}
@Override
public String toString() {
return "PersonEntity{" + "id=" + id + ", firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", ssn='" + ssn + '\'' + ", bloodType='" + bloodType + '\'' + '}';
}
public ObjectId getId() {
return id;
}
public void setId(ObjectId id) {
this.id = id;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getSsn() {
return ssn;
}
public void setSsn(String ssn) {
this.ssn = ssn;
}
public String getBloodType() {
return bloodType;
}
public void setBloodType(String bloodType) {
this.bloodType = bloodType;
}
}

前面的类包含自动化 CSFLE 所需的所有信息。该类通过以下方式使用 @Encrypted 注解:

  • @Encrypted 类注解包含 SpEL表达式#{mongocrypt.keyId(#target)}。此表达式指定您在上一步中生成或检索的 DEK 的 keyId。 CSFLE 使用此 DEK 进行加密。

  • ssn字段上的 @Encrypted 注解指定它需要确定性算法。

  • bloodType字段上的 @Encrypted 注解指定它需要随机算法。

Spring Data MongoDB为此实体生成的JSON schema 如下:

{
"encryptMetadata": {
"keyId": [
{
"$binary": {
"base64": "WyHXZ+53SSqCC/6WdCvp0w==",
"subType": "04"
}
}
]
},
"type": "object",
"properties": {
"ssn": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"
}
},
"bloodType": {
"encrypt": {
"bsonType": "string",
"algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Random"
}
}
}
}

EntitySpelEvaluationExtension 类支持对 SpEL表达式求值。以下代码显示了该类:

@Component
public class EntitySpelEvaluationExtension implements EvaluationContextExtension {
private static final Logger LOGGER = LoggerFactory.getLogger(EntitySpelEvaluationExtension.class);
private final DataEncryptionKeyService dataEncryptionKeyService;
public EntitySpelEvaluationExtension(DataEncryptionKeyService dataEncryptionKeyService) {
this.dataEncryptionKeyService = dataEncryptionKeyService;
}
@Override
@NonNull
public String getExtensionId() {
return "mongocrypt";
}
@Override
@NonNull
public Map<String, Function> getFunctions() {
try {
return Collections.singletonMap("keyId", new Function(
EntitySpelEvaluationExtension.class.getMethod("computeKeyId", String.class), this));
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
public String computeKeyId(String target) {
String dek = dataEncryptionKeyService.getDataEncryptionKeysB64().get(target);
LOGGER.info("=> Computing dek for target {} => {}", target, dek);
return dek;
}
}

注意

该类检索 DEK 并将它们与 target 进行匹配。在此示例中,targetPersonEntity

以下部分介绍如何在 Spring Data MongoDB项目中生成JSON schema。

要生成JSON schema,您需要通过 Spring Data 自动配置创建的 MappingContext 实体。自动配置会创建 MongoClient 连接和 MongoTemplate

但是,要创建启用自动加密的 MongoClient,您需要JSON schema。

解决方案是通过实例化 MongoClientSettingsBuilderCustomizer bean,在自动配置进程中注入JSON schema 创建。

以下代码显示了 MongoDBSecureClientConfiguration 类:

@Configuration
@DependsOn("keyVaultAndDekSetup")
public class MongoDBSecureClientConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(MongoDBSecureClientConfiguration.class);
private final KmsService kmsService;
private final SchemaService schemaService;
@Value("${crypt.shared.lib.path}")
private String CRYPT_SHARED_LIB_PATH;
@Value("${spring.data.mongodb.storage.uri}")
private String CONNECTION_STR_DATA;
@Value("${spring.data.mongodb.vault.uri}")
private String CONNECTION_STR_VAULT;
@Value("${mongodb.key.vault.db}")
private String KEY_VAULT_DB;
@Value("${mongodb.key.vault.coll}")
private String KEY_VAULT_COLL;
private MongoNamespace KEY_VAULT_NS;
public MongoDBSecureClientConfiguration(KmsService kmsService, SchemaService schemaService) {
this.kmsService = kmsService;
this.schemaService = schemaService;
}
@PostConstruct
public void postConstruct() {
this.KEY_VAULT_NS = new MongoNamespace(KEY_VAULT_DB, KEY_VAULT_COLL);
}
@Bean
public MongoClientSettings mongoClientSettings() {
LOGGER.info("=> Creating the MongoClientSettings for the encrypted collections.");
return MongoClientSettings.builder().applyConnectionString(new ConnectionString(CONNECTION_STR_DATA)).build();
}
@Bean
public MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) {
LOGGER.info("=> Creating the MongoClientSettingsBuilderCustomizer.");
return builder -> {
MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext);
Map<String, BsonDocument> schemaMap = schemaService.generateSchemasMap(schemaCreator)
.entrySet()
.stream()
.collect(toMap(e -> e.getKey().getFullName(),
Map.Entry::getValue));
Map<String, Object> extraOptions = Map.of("cryptSharedLibPath", CRYPT_SHARED_LIB_PATH,
"cryptSharedLibRequired", true);
MongoClientSettings mcs = MongoClientSettings.builder()
.applyConnectionString(
new ConnectionString(CONNECTION_STR_VAULT))
.build();
AutoEncryptionSettings oes = AutoEncryptionSettings.builder()
.keyVaultMongoClientSettings(mcs)
.keyVaultNamespace(KEY_VAULT_NS.getFullName())
.kmsProviders(kmsService.getKmsProviders())
.schemaMap(schemaMap)
.extraOptions(extraOptions)
.build();
builder.autoEncryptionSettings(oes);
};
}
}

提示

如果想对不同的集群使用不同的备份保留策略,可以在每个集群中存储一个 DEK。这使您可以从集群和所有备份中完全擦除 DEK,这可能是法律合规所必需的。

有关更多信息,请参阅使用Java驱动程序的 CSFLE。

以下代码显示了 SchemaServiceImpl 类,它将生成的JSON schema存储在地图中:

@Service
public class SchemaServiceImpl implements SchemaService {
private static final Logger LOGGER = LoggerFactory.getLogger(SchemaServiceImpl.class);
private Map<MongoNamespace, BsonDocument> schemasMap;
@Override
public Map<MongoNamespace, BsonDocument> generateSchemasMap(MongoJsonSchemaCreator schemaCreator) {
LOGGER.info("=> Generating schema map.");
List<EncryptedEntity> encryptedEntities = EncryptedCollectionsConfiguration.encryptedEntities;
return schemasMap = encryptedEntities.stream()
.collect(toMap(EncryptedEntity::getNamespace,
e -> generateSchema(schemaCreator, e.getEntityClass())));
}
@Override
public Map<MongoNamespace, BsonDocument> getSchemasMap() {
return schemasMap;
}
private BsonDocument generateSchema(MongoJsonSchemaCreator schemaCreator, Class<?> entityClass) {
BsonDocument schema = schemaCreator.filter(MongoJsonSchemaCreator.encryptedOnly())
.createSchemaFor(entityClass)
.schemaDocument()
.toBsonDocument();
LOGGER.info("=> JSON Schema for {}:\n{}", entityClass.getSimpleName(),
schema.toJson(JsonWriterSettings.builder().indent(true).build()));
return schema;
}
}

应用程序会存储其JSON schema,因为此模板还实现了服务器端JSON schema。这是 CSFLE 的最佳实践。

自动加密共享库仅需要客户端JSON schema。但是,如果没有服务器端JSON schema,另一个配置错误的客户端或直接连接到集群的管理员可以在不加密字段的情况下插入或更新文档。

为防止这种情况,您可以使用服务器端JSON schema 在文档中实施字段类型。

JSON schema随应用程序版本的不同而变化,因此每次重新启动应用程序时都需要更新JSON schema。

以下代码显示了 EncryptedCollectionsSetup 类:

@Component
public class EncryptedCollectionsSetup {
private static final Logger LOGGER = LoggerFactory.getLogger(EncryptedCollectionsSetup.class);
private final MongoClient mongoClient;
private final SchemaService schemaService;
public EncryptedCollectionsSetup(MongoClient mongoClient, SchemaService schemaService) {
this.mongoClient = mongoClient;
this.schemaService = schemaService;
}
@PostConstruct
public void postConstruct() {
LOGGER.info("=> Setup the encrypted collections.");
schemaService.getSchemasMap()
.forEach((namespace, schema) -> createOrUpdateCollection(mongoClient, namespace, schema));
}
private void createOrUpdateCollection(MongoClient mongoClient, MongoNamespace ns, BsonDocument schema) {
MongoDatabase db = mongoClient.getDatabase(ns.getDatabaseName());
String collStr = ns.getCollectionName();
if (doesCollectionExist(db, ns)) {
LOGGER.info("=> Updating {} collection's server side JSON Schema.", ns.getFullName());
db.runCommand(new Document("collMod", collStr).append("validator", jsonSchemaWrapper(schema)));
} else {
LOGGER.info("=> Creating encrypted collection {} with server side JSON Schema.", ns.getFullName());
db.createCollection(collStr, new CreateCollectionOptions().validationOptions(
new ValidationOptions().validator(jsonSchemaWrapper(schema))));
}
}
public BsonDocument jsonSchemaWrapper(BsonDocument schema) {
return new BsonDocument("$jsonSchema", schema);
}
private boolean doesCollectionExist(MongoDatabase db, MongoNamespace ns) {
return db.listCollectionNames()
.into(new ArrayList<>())
.stream()
.anyMatch(c -> c.equals(ns.getCollectionName()));
}
}

本指南中的模板可以支持任意数量的实体。

要添加其他类型的实体,请按照前面的步骤所述创建三层架构的组件。然后,将该实体添加到 EncryptedCollectionsConfiguration 类中,如以下示例所示:

public class EncryptedCollectionsConfiguration {
public static final List<EncryptedEntity> encryptedEntities = List.of(
new EncryptedEntity("mydb", "persons", PersonEntity.class, "personDEK"),
new EncryptedEntity("mydb", "companies", CompanyEntity.class, "companyDEK"));
}

由于您将 @Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") 注解添加到实体类中的字段,因此该框架使用服务器端JSON schema 自动生成 DEK 并创建加密集合。

此模板实施 findFirstBySsn(ssn) 方法。您可以使用此方法通过人员的社会安全号查找其文档,即使此字段已加密。

注意

此方法仅在使用确定性加密算法时才有效。

以下代码显示了 PersonRepository 接口:

@Repository
public interface PersonRepository extends MongoRepository<PersonEntity, String> {
PersonEntity findFirstBySsn(String ssn);
}

如有疑问,请在 mongodb-java-spring-boot-csfle存储库中提出问题或在MongoDB Community论坛中提问。

要学习;了解有关 CSFLE 的更多信息,请参阅以下资源:

要学习;了解有关 Spring Data MongoDB 的更多信息,请参阅以下资源:

您可以在MongoDB YouTube渠道上观看本指南的视频版本。

后退

开始使用 Spring Session MongoDB