Docs Menu
Docs Home
/ /

Implementar CSFLE en Java mediante Spring Data MongoDB

Esta guía le muestra cómo integrar MongoDB Cifrado a nivel de campo del lado del cliente(CSFLE) con Spring Data MongoDB.

Nota

Si no ha trabajado con CSFLE antes, consulte la guía de tutoriales de CSFLE en el manual del servidor MongoDB.

Si no ha trabajado con Spring Data MongoDB antes, consulte la documentación de Spring Data MongoDB.

Para ver el código fuente de esta guía, consulte el repositorio mongodb-java-spring-boot-csfle en GitHub.

También puede recuperar el código fuente de su repositorio de GitHub ejecutando el siguiente comando:

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

Antes de utilizar esta guía, realice los siguientes pasos:

  1. Instalar Java 17

  2. Instalar la biblioteca compartida de cifrado automático de MongoDB v7.0.2 o posterior

  3. Revise el archivo README.md para obtener información sobre cómo ejecutar el código de ejemplo.

También necesita acceso a un clúster MongoDB que ejecute MongoDB v7.0.2 o posterior.

Este ejemplo proporciona código con plantilla para un entorno de producción e incluye las siguientes características:

  • Múltiples colecciones cifradas

  • Generación automatizada de esquemas JSON

  • Esquema JSON del lado del servidor

  • Clústeres separados para claves de cifrado de datos (DEK) y colecciones cifradas

  • Generación o recuperación automatizada de DEK

  • Extensión de evaluación del lenguaje de expresión Spring (SpEL)

  • Repositorios implementados automáticamente

  • Documentación de OpenAPI 3.0.1

La plantilla sigue los principios SOLID para facilitar la lectura y la reutilización del código. Para obtener más información sobre SOLID, consulte SOLID en Wikipedia.

El siguiente diagrama muestra los componentes necesarios para crear un sistema habilitado para CSFLE. MongoClient Que puede cifrar y descifrar campos automáticamente. Las flechas del diagrama muestran las dependencias y relaciones entre los componentes.

Diagrama de alto nivel del proyecto

Las flechas en el diagrama muestran dependencias y relaciones entre componentes.

Una vez que la aplicación establece una conexión con MongoDB, utiliza una arquitectura de tres niveles para exponer una API REST y administrar la comunicación con la base de datos MongoDB, como se muestra en el siguiente diagrama:

Arquitectura de tres niveles

Esta sección le muestra cómo crear la colección de almacén de claves y su índice único.

Si está comenzando desde un nuevo clúster de MongoDB, primero debe crear la colección de almacén de claves y su índice único en el campo keyAltNames.

La clase KeyVaultAndDekSetup crea la colección de almacén de claves y su índice único mediante una conexión estándar a MongoDB. A continuación, crea las DEK necesarias para cifrar los documentos de cada colección cifrada.

El siguiente código muestra la clase 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);
}
}
}

Importante

En producción, puede crear manualmente la colección de almacén de claves y su índice único en el campo keyAltNames. Después de crearlos, puede eliminar este código.

Este código utiliza una conexión MongoClient estándar (no habilitada para CSFLE) y temporal en un bloque trycon recursos para crear una colección y un índice en el clúster MongoDB.

La clase KeyVaultServiceImpl crea la colección de almacén de claves y el índice único keyAltNames.

El siguiente código muestra la clase 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));
}
}

Después de crear la colección y el índice del almacén de claves, puede cerrar la conexión estándar de MongoDB.

Esta sección le muestra cómo crear las DEK mediante la conexión ClientEncryption.

La clase MongoDBKeyVaultClientConfiguration crea un bean ClientEncryption que DataEncryptionKeyService utiliza para crear los DEK.

El siguiente código muestra la clase 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);
}
}

Puedes crear un ClientEncryption bean mediante el Sistema de Gestión de Claves (KMS) y usarlo para generar las DEK. Necesitas una DEK por cada colección cifrada.

La clase DataEncryptionKeyServiceImpl crea y almacena las DEK. Al evaluar las expresiones SpEL en las entidades para crear los esquemas JSON, debe recuperar las DEK.

El siguiente código muestra la clase 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;
}
}

Nota

Debido a que los DEK se almacenan en un mapa, no es necesario recuperarlos nuevamente más tarde para los esquemas JSON.

Spring Data MongoDB utiliza un modelo centrado en POJO para implementar los repositorios y mapear los documentos a las colecciones de MongoDB.

El siguiente código muestra la clase 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;
}
}

La clase anterior contiene toda la información necesaria para automatizar CSFLE. La clase utiliza la anotación @Encrypted de las siguientes maneras:

  • La anotación de clase @Encrypted contiene la expresión SpEL #{mongocrypt.keyId(#target)}. Esta expresión especifica el keyId de la DEK generada o recuperada en un paso anterior. CSFLE utiliza esta DEK para el cifrado.

  • La anotación @Encrypted en el campo ssn especifica que requiere un algoritmo determinista.

  • La anotación @Encrypted en el campo bloodType especifica que requiere un algoritmo aleatorio.

El esquema JSON generado por Spring Data MongoDB para esta entidad es el siguiente:

{
"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"
}
}
}
}

La clase EntitySpelEvaluationExtension permite la evaluación de la expresión SpEL. El siguiente código muestra esta clase:

@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;
}
}

Nota

Esta clase recupera las DEK y las compara con target. En este ejemplo, target es PersonEntity.

Las siguientes secciones muestran cómo generar esquemas JSON en un proyecto Spring Data MongoDB.

Para generar los esquemas JSON, se necesitan las entidades MappingContext creadas mediante la configuración automática de Spring Data. Esta configuración automática crea la conexión MongoClient y la MongoTemplate.

Sin embargo, para crear el MongoClient con el cifrado automático habilitado, necesita esquemas JSON.

La solución es inyectar la creación del esquema JSON en el proceso de autoconfiguración instanciando el bean MongoClientSettingsBuilderCustomizer.

El siguiente código muestra la clase 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);
};
}
}

Tip

Si desea utilizar diferentes políticas de retención de copias de seguridad para distintos clústeres, puede almacenar una DEK en cada clúster. Esto le permite borrar completamente una DEK de un clúster y todas las copias de seguridad, lo cual podría ser necesario para el cumplimiento legal.

Para obtenermás información, consulte CSFLE con el controlador Java.

El siguiente código muestra la clase SchemaServiceImpl, que almacena los esquemas JSON generados en un mapa:

@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;
}
}

La aplicación almacena sus esquemas JSON porque esta plantilla también implementa esquemas JSON del lado del servidor. Esta es una práctica recomendada para CSFLE.

Solo se requieren los esquemas JSON del lado del cliente para la biblioteca compartida de cifrado automático. Sin embargo, sin los esquemas JSON del lado del servidor, otro cliente mal configurado o un administrador conectado directamente al clúster podría insertar o actualizar documentos sin cifrar los campos.

Para evitar esto, puede utilizar el esquema JSON del lado del servidor para imponer un tipo de campo en un documento.

El esquema JSON cambia con las diferentes versiones de su aplicación, por lo que es necesario actualizarlo cada vez que reinicie su aplicación.

El siguiente código muestra la clase 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()));
}
}

La plantilla de esta guía puede admitir cualquier número de entidades.

Para agregar otro tipo de entidad, cree los componentes de la arquitectura de tres niveles como se describe en los pasos anteriores. Luego, agregue la entidad a la clase EncryptedCollectionsConfiguration, como se muestra en el siguiente ejemplo:

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

Debido a que agregó la anotación @Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") a los campos en la clase de entidad, el marco utiliza el esquema JSON del lado del servidor para generar automáticamente las DEK y crear la colección cifrada.

Esta plantilla implementa el método findFirstBySsn(ssn). Puede usar este método para buscar el documento de una persona por su número de Seguro Social, incluso si este campo está cifrado.

Nota

Este método funciona únicamente porque se utiliza un algoritmo de cifrado determinista.

El siguiente código muestra la interfaz PersonRepository:

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

Si tiene preguntas, abra un problema en el repositorio mongodb-java-spring-boot-csfle o haga una pregunta en el Foro de la comunidad MongoDB.

Para obtener más información sobre CSFLE, consulte los siguientes recursos:

Para obtener más información sobre Spring Data MongoDB, consulte los siguientes recursos:

Puedes ver una versión de esta guía en formato de vídeo en el canal de YouTube de MongoDB.

Volver

Introducción a Spring Session MongoDB