Learn the "why" behind slow queries and how to fix them in our 2-Part Webinar.
Register now >
Menu Docs
Página inicial do Docs
/ /

Implemente o CSFLE em Java usando o Spring Data MongoDB

Este guia mostra como integrar o MongoDB criptografia no nível do campo do lado do cliente (CSFLE) com o Spring Data MongoDB.

Observação

Se você ainda não trabalhou com o CSFLE, consulte o guia de tutoriais do CSFLE no manual do MongoDB Server.

Se você ainda não trabalhou com o Spring Data MongoDB, consulte a documentação do Spring Data MongoDB.

Para visualizar o código-fonte deste guia, consulte o repositório mongodb-java-spring-boot-csfle no GitHub.

Você também pode recuperar o código fonte de seu repositório GitHub executando o seguinte comando:

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

Antes de usar este guia, execute as seguintes etapas:

  1. Instalar Java 17

  2. Instale a Biblioteca compartilhada de criptografia automática do MongoDB v7.0.2 ou posterior

  3. Revise o README.md para obter informações sobre como executar o código de exemplo.

Você também precisa de acesso a um cluster do MongoDB que esteja executando o MongoDB v7.0.2 ou posterior.

Este exemplo fornece código modelo para um ambiente de produção e inclui as seguintes funcionalidades:

  • Várias coleções criptografadas

  • Geração automatizada de JSON schema

  • JSON schema do lado do servidor

  • Clusters separados para chaves de criptografia de dados (DEKs) e coleções criptografadas

  • Geração ou recuperação automatizada de DEK

  • Extensão de avaliação da Spring Expression Language (SpEL)

  • Repositórios implementados automaticamente

  • OpenAPI 3.0.1 documentação

O modelo segue os princípios SOLID para aumentar a legibilidade e a reutilização do código. Para saber mais sobre o SOLID, veja SOLID na Wikipedia.

O diagrama a seguir mostra os componentes necessários para criar um MongoClient habilitado para CSFLE que pode criptografar e descriptografar campos automaticamente. As setas no diagrama mostram dependências e relacionamentos entre componentes.

Diagrama de alto nível do projeto

As setas no diagrama mostram dependências e relacionamentos entre componentes.

Depois que o aplicativo estabelece uma conexão com o MongoDB , ele usa uma arquitetura de três camadas para expor uma REST API e gerenciar a comunicação com o banco de dados do MongoDB , conforme mostrado no diagrama a seguir:

Arquitetura de três níveis

Esta seção mostra como criar a coleção de cofre de chaves e seu índice único.

Se você estiver iniciando a partir de um novo cluster MongoDB , deverá primeiro criar a coleção de cofre de chaves e seu índice único no campo keyAltNames.

A classe KeyVaultAndDekSetup cria a coleção de cofre de chaves e seu índice único usando uma conexão padrão com o MongoDB. Em seguida, ele cria os DEKs necessários para criptografar os documentos em cada coleção criptografada.

O seguinte código mostra a classe 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

Na produção, você pode criar manualmente a coleção de cofre de chaves e seu índice único no campo keyAltNames . Depois de criá-los, você pode remover esse código.

Este código usa uma conexão MongoClient padrão (não habilitada para CSFLE) e temporária em um bloco try-with-resources para criar uma coleção e um índice no cluster MongoDB .

A classe KeyVaultServiceImpl cria a coleção de cofre de chaves e o keyAltNames índice único.

O seguinte código mostra a classe 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));
}
}

Depois de criar a coleção de cofre de chaves e o índice, você pode fechar a conexão padrão do MongoDB.

Esta seção mostra como criar os DEKs utilizando a conexão do ClientEncryption.

A classe MongoDBKeyVaultClientConfiguration cria um bean ClientEncryption que o DataEncryptionKeyService utiliza para criar os DEKs.

O seguinte código mostra a classe 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);
}
}

Você pode criar um bean ClientEncryption usando o Sistema de Gerenciamento de Chaves (KMS) e usá-lo para gerar os DEKs. Você precisa de um DEK para cada coleção criptografada.

A classe DataEncryptionKeyServiceImpl cria e armazena os DEKs. Ao avaliar as expressões SpEL nas entidades para criar os JSON schemas, você deve recuperar os DEKs.

O seguinte código mostra a classe 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;
}
}

Observação

Como as DEKs são armazenadas em um mapa, você não precisa recuperá-las novamente mais tarde para os JSON esquemas.

O Spring Data MongoDB usa um modelo baseado em POJO para implementar os repositórios e mapear os documentos para as coleções do MongoDB.

O seguinte código mostra a classe 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;
}
}

A classe anterior contém todas as informações necessárias para automatizar o CSFLE. A classe utiliza a anotação @Encrypted das seguintes formas:

  • A anotação de classe @Encrypted contém a expressão SpEL #{mongocrypt.keyId(#target)}. Esta expressão especifica o keyId do DEK que você gerou ou recuperou em uma etapa anterior. O CSFLE utiliza esta DEK para criptografia.

  • A anotação @Encrypted no campo ssn especifica que ele requer um algoritmo determinístico.

  • A anotação @Encrypted no campo bloodType especifica que ele requer um algoritmo aleatório.

O JSON Schema gerado pelo Spring Data MongoDB para esta entidade é o seguinte:

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

A classe EntitySpelEvaluationExtension habilita a avaliação da expressão SpEL. O seguinte código mostra esta classe:

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

Observação

Esta classe recupera os DEKs e os combina com o target. Neste exemplo, o target é PersonEntity.

As seções a seguir mostram como gerar JSON esquemas em um projeto Spring Data MongoDB.

Para gerar os JSON esquemas, você precisa das entidades MappingContext que são criadas pela configuração automática do Spring Data. A configuração automática cria a conexão MongoClient e o MongoTemplate.

No entanto, para criar o MongoClient com a criptografia automática ativada, você precisa de JSON esquemas.

A solução é injetar a criação do JSON schema no processo de configuração automática instanciando o bean MongoClientSettingsBuilderCustomizer.

O seguinte código mostra a classe 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);
};
}
}

Dica

Se você quiser usar diferentes políticas de retenção de backup para clusters diferentes, poderá armazenar uma DEK em cada cluster. Isso permite que você apague totalmente uma DEK de um cluster e todos os backups, o que pode ser necessário para a conformidade legal.

Para obter mais informações, consulte CSFLE com o Java Driver.

O seguinte código mostra a classe SchemaServiceImpl , que armazena os JSON schemas gerados em um 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;
}
}

O aplicativo armazena seus JSON esquemas porque esse modelo também implementa JSON esquemas do lado do servidor. Esta é uma melhor prática para o CSFLE.

Somente os JSON schemas do lado do cliente são necessários para a Biblioteca compartilhada de criptografia automática. No entanto, sem JSON schemas do lado do servidor, outro cliente mal configurado ou um administrador conectado diretamente ao cluster poderia inserir ou atualizar documentos sem criptografar os campos.

Para evitar isso, você pode usar o JSON schema do lado do servidor para impor um tipo de campo em um documento.

O JSON Schema muda com as diferentes versões do seu aplicativo, portanto, os JSON Schemas precisam ser atualizados sempre que você reiniciar o aplicativo.

O seguinte código mostra a classe 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()));
}
}

O modelo neste guia pode oferecer suporte a qualquer número de entidades.

Para adicionar outro tipo de entidade, crie os componentes da arquitetura de três níveis conforme descrito nas etapas anteriores. Em seguida, adicione a entidade à classe EncryptedCollectionsConfiguration, como mostrado no seguinte exemplo:

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

Como você adicionou a anotação @Encrypted(algorithm = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic") aos campos na classe de entidade, o framework usa o JSON Schema do lado do servidor para gerar automaticamente os DEKs e criar a coleção criptografada.

Este modelo implementa o método findFirstBySsn(ssn). Você pode usar esse método para encontrar o documento de uma pessoa pelo número do seguro social, mesmo que esse campo esteja criptografado.

Observação

Esse método funciona somente porque um algoritmo de criptografia determinístico é usado.

O seguinte código mostra a interface PersonRepository:

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

Se você tiver dúvidas, abra um problema no repositório mongodb-java-spring-boot-csfle ou faça uma pergunta no Fórum da MongoDB Community.

Para saber mais sobre o CSFLE, consulte os seguintes recursos:

Para saber mais sobre o Spring Data MongoDB, consulte os seguintes recursos:

Você pode ver uma versão deste guia em formato de vídeo no canal do MongoDB no YouTube.

Voltar

Fragmentação na inicialização do Spring com dados do Spring