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

String Data MongoDBを使用してJavaに 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. Java 17 のインストール

  2. MongoDB自動暗号化共有ライブラリ v7.0.2 以降をインストール

  3. 例コードの実行方法については、README.md を参照してください。

また、MongoDB v7.0.2 以降を実行しているMongoDBクラスターにもアクセスする必要があります。

この例では、本番環境向けのテンプレート コードを提供しており、次の機能を含めています。

  • 複数の暗号化されたコレクション

  • 自動JSON Schema 生成

  • サーバーサイドJSON schema

  • データ暗号化キー(DEK)と暗号化されたコレクションの分離されたクラスター

  • 自動 DEK 生成または取得

  • プロセス式言語(SpEL)の評価拡張機能

  • 自動実装リポジトリ

  • OpenAPI 3.0.1ドキュメント

テンプレートは、コードの読みやすさと再利用を高めるために SOLID の原則に従います。SOLID の詳細については、SOLID の Wikipedia を参照してください。

次の図は、フィールドを自動的に暗号化および復号化できる CSFLE 対応の MongoClient を作成するために必要なコンポーネントを示しています。図の矢印は、コンポーネント間の依存関係と関係を示しています。

プロジェクト概要図

図の矢印は、コンポーネント間の依存関係と関係を示しています。

アプリケーションがMongoDBとの接続を確立すると、次の図に示すように、3 階層アーキテクチャを使用してREST API を公開し、 MongoDBデータベースとの通信を管理します。

3 階層アーキテクチャ

このセクションでは、キーヴォールトコレクションとそのユニークインデックスを作成する方法について説明します。

新しい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フィールドにキーヴォールトコレクションとそのユニークインデックスを手動で作成できます。これらを作成した後、このコードを除くことができます。

このコードでは、標準(CSFLE が有効になっていない)と一時的な MongoClient 接続を使用して、try と リソース ブロックを使用して、 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クラスは、DataEncryptionKeyService が DEK の作成に使用する ClientEncryption Bearer を作成します。

次のコードは、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);
}
}

KMS(Key Management System)とそれを使用してDEKを生成することで、ClientEncryption bean を作成できます。暗号化されたコレクションごとに 1 つの DEK が必要です。

DataEncryptionKeyServiceImplクラスはDEK を作成して保存します。エンティティ内の SPEL 式を評価してJSONスキーマを作成する場合は、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スキーマに対して後で再度検索する必要はありません。

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クラスの注釈には SEL式#{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スキーマを生成する方法を示します。

JSONスキーマを生成するには、spring Data の自動構成によって作成される MappingContext エンティティが必要です。自動構成により、MongoClient 接続と MongoTemplate が作成されます。

ただし、自動暗号化を有効にして MongoClient を作成するには、 JSONスキーマが必要です。

解決策としては、MongoClientSettingsBuilderCustomizer beans をインスタンス化して、自動構成プロセスに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);
};
}
}

Tip

異なるクラスターに対して異なるバックアップ保持ポリシーを使用する場合は、各クラスターに 1 つの DEK を保存できます。これにより、クラスターとすべてのバックアップから DEK を完全に削除できます。これは、コンプライアンスに必要になる場合があります。

詳細については、CSFLE とJavaドライバー を参照してください。

次のコードは、生成されたJSONスキーマをマップに保存する SchemaServiceImplクラスを示しています。

@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 スキーマ も実装しているため、アプリケーションはJSON スキーマ を保存します。これは CSFLE のベストプラクティスです。

自動暗号化共有ライブラリには、クライアント側のJSON スキーマ のみが必要です。ただし、サーバー側のJSON スキーマ を使用しないと、別の誤ったクライアントまたはクラスターに直接接続された管理者が、フィールドを暗号化せずにドキュメントを挿入または更新することができます。

これを防ぐために、サーバー側の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()));
}
}

このガイドのテンプレートは、任意の数のエンティティをサポートできます。

別のタイプのエンティティを追加するには、前の手順で説明したように 3 階層アーキテクチャのコンポーネントを作成します。次に、次の例に示すように、エンティティを 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 チャンネル でビデオ形式で見ることができます。

戻る

を使用したレプリカセットでのシャーディング