spring-boot-starter-data-mongodb-reactive setting keystore password from application.yml 使用 X509 连接
spring-boot-starter-data-mongodb-reactive setting keystore password from application.yml for connecting using X509
为了通过验证为 X509 用户使用反应流连接到 mongodb,mongodb 驱动程序强制设置两个 jvm 属性:
javax.net.ssl.keyStore
javax.net.ssl.keyStorePassword
https://mongodb.github.io/mongo-java-driver/4.0/driver-reactive/tutorials/ssl/
我只能设置属性并使其工作在应用程序启动之前
System.setProperty("javax.net.ssl.keyStore", "path");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SpringApplication.run(ChgQuerySvcApplication.class, args);
但是,如果我尝试在扩展 AbstractReactiveMongoConfiguration
的 class 中设置这些属性
它不接听。
@Configuration
public class ReactiveMongoConfiguration extends AbstractReactiveMongoConfiguration {
@Autowired
Environment environment;
@Value("${mypassword}")
private String keyStorePassword;
@Override
public MongoClient reactiveMongoClient() {
MongoProperties properties = new MongoProperties();
properties.setDatabase("somdedb");
String uri = "mongodb+srv://CN=username@clusteraddress/somedb?authSource=%24external&authMechanism=MONGODB-X509&retryWrites=true&w=majority";
properties.setUri(uri);
ReactiveMongoClientFactory factory = new ReactiveMongoClientFactory(properties, environment, null);
System.setProperty("javax.net.ssl.keyStore", "path to key store");
System.setProperty("javax.net.ssl.keyStorePassword", "password"); // possibly replace with keyStorePassword
MongoCredential credential = MongoCredential.createMongoX509Credential("CN=username"); // redundant, I know
MongoClientSettings settings = MongoClientSettings.builder()
.applyToSslSettings(builder -> builder
.applySettings(SslSettings.builder().enabled(true).invalidHostNameAllowed(true).build()))
.credential(credential).build();
return factory.createMongoClient(settings);
}
}
我正在使用的 spring 入门依赖项(版本 2.3.2.RELEASE):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
当我尝试连接时,出现以下异常:
{"@timestamp":"2020-08-07T17:34:18.346-05:00","logger_name":"org.mongodb.driver.client","thread_name":"async-channel-group-0-handler-executor","severity":"ERROR","trace":"","span":"","parent":"","message":"Calling onError threw an exception","stack_trace":"com.mongodb.MongoCommandException: Command failed with error 18 (AuthenticationFailed): 'No verified subject name available from client' on server servername:27017. The full response is {\"operationTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"ok\": 0.0, \"errmsg\": \"No verified subject name available from client\", \"code\": 18, \"codeName\": \"AuthenticationFailed\", \"$clusterTime\": {\"clusterTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"signature\": {\"hash\": {\"$binary\": {\"base64\": \"4IS/JaRasdauyWO9aXVOcaHm2s+3KzKg=\", \"subType\": \"00\"}}, \"keyId\": 123234}}}\r\n\tat com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)\r\n\tat com.mongodb.internal.connection.InternalStreamConnection.onResult(InternalStreamConnection.java:389)\r\n\t... 13 common frames omitted\r\nWrapped by: com.mongodb.MongoSecurityException: Exception authenticating\r\n\tat com.mongodb.internal.connection.X509Authenticator.translat...\r\n"}
{"@timestamp":"2020-08-07T17:34:18.347-05:00","logger_name":"org.mongodb.driver.client","thread_name":"async-channel-group-0-handler-executor","severity":"ERROR","trace":"","span":"","parent":"","message":"Callback onResult call produced an error","stack_trace":"com.mongodb.MongoCommandException: Command failed with error 18 (AuthenticationFailed): 'No verified subject name available from client' on server servername:27017. The full response is {\"operationTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"ok\": 0.0, \"errmsg\": \"No verified subject name available from client\", \"code\": 18, \"codeName\": \"AuthenticationFailed\", \"$clusterTime\": {\"clusterTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"signature\": {\"hash\": {\"$binary\": {\"base64\": \"4IS/asdJaRuyaWO9XVOcaHm2s+3KzKg=\", \"subType\": \"00\"}}, \"keyId\": 123234}}}\r\n\tat com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)\r\n\tat com.mongodb.internal.connection.InternalStreamConnection.onResult(InternalStreamConnection.java:389)\r\n\t... 13 common frames omitted\r\nWrapped by: com.mongodb.MongoSecurityException: Exception authenticating\r\n\tat com.mongodb.internal.connection.X509Authenticator.translat...\r\n"}
我尝试这样做的原因是我可以通过 spring 云配置设置密码,而不是硬编码或作为 JVM 参数传递。有没有办法动态设置这些属性?
x.509 身份验证所需的所有选项都应在最新驱动程序的连接字符串中指定。 各种语言的示例。
- 学习connection string documentation.
- 构造一个包含所有选项的连接字符串。
- 使用此连接字符串通过
mongo
shell 连接到您的部署。不要使用 command-line 参数传递任何选项,仅使用连接字符串。
- 使用与您的驱动程序相同的连接字符串。
要解决身份验证错误,请阅读 server log。
tl;dr
在创建 MongoDB 连接之前,我们能够使用 springboot 的 MongoClientSettingsBuilderCustomizer 向 MongoDB 连接工厂提供新的 SSLContext,实质上是覆盖 JVM 中可用的默认 SSLContext
详细解释:
MongoDB 的 Java 驱动程序完全依赖于 JRE 中的 SSLContext
,因此无法
通过连接字符串等设置它。我通过 MongoDB 支持确认了这一点。由于我们利用 spring-boot-starter-data-mongodb-reactive
。我们能够使用 spring-boot 提供的一些定制器。以下是我们的解决方法:
我们创建了定制器的 bean:
@Configuration
@RequiredArgsConstructor
public class MongoX509CredentialClientSettingsBuilderCustomizer implements MongoClientSettingsBuilderCustomizer {
private static final String MONGO_KEY_ENTRY_ALIAS = "mongo-client-key";
private static final JcaX509CertificateConverter X509_CERTIFICATE_CONVERTER = new JcaX509CertificateConverter();
private static final JcaPEMKeyConverter PEM_KEY_CONVERTER = new JcaPEMKeyConverter();
private final MongoX509Properties properties;
/**
* Only customizes the {@link SslSettings} for use with X.509 Certificate
* Authentication
*/
@Override
public void customize(Builder clientSettingsBuilder) {
// @formatter:off
clientSettingsBuilder
.applyToSslSettings(builder -> builder
.applySettings(SslSettings
.builder()
.context(sslContext())
.enabled(true)
.build()))
.credential(MongoCredential.createMongoX509Credential());
// @formatter:on
}
/**
* Creates an {@link SSLContext} that can connect to any endpoint exposing a
* valid well known CA by the JRE. And uses a dynamic array of
* {@link KeyManager} that contains the {@link X509Certificate} and
* {@link PrivateKey} configured for use with a MongoDB instance.
*
* @return
*/
@SneakyThrows
public SSLContext sslContext() {
SSLContext sslContext = SSLContext.getInstance(properties.getTlsVersion());
sslContext.init(keyManagers(x509Certificate(), privateKey()), trustManagers(), null);
return sslContext;
}
/**
* Creates an array of {@link TrustManager} containing the default set of
* trusted certificate authorities. This is required to make a TLS connection to
* the MongoDB instance. If MongoDB is Atlas then the CA is Let's Encrypt and
* should already be trusted so copy that over to the SSLContext that we are
* creating.
*
* @return an array of {@link TrustManager} initialized with the default trust
* managers.
*/
@SneakyThrows
private TrustManager[] trustManagers() {
TrustManagerFactory defaultTrustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Using null here init the trustManagerFactory with the default trust store.
defaultTrustManagerFactory.init((KeyStore) null);
// only need the default trust managers if the CA for mongo is already in the
// default trust store for the JVM (e.g. via bosh managed trust store or via the
return defaultTrustManagerFactory.getTrustManagers();
}
/**
* Creates an array of {@link KeyManager} containing the certificate and
* privateKey provided in a in memory only {@link KeyStore} with alias
* {@link #MONGO_KEY_ENTRY_ALIAS} used for x509 authentication. This is a dymaic
* key manager containing the private key and certificate in the store for use
* by the {@link SSLContext} that this class creates.
*
* @param certificate
* @param privateKey
* @return an array of {@link KeyManager} initialized with the in memory
* {@link KeyStore}.
*/
@SneakyThrows
private KeyManager[] keyManagers(X509Certificate certificate, PrivateKey privateKey) {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null); // You don't need the KeyStore instance to come from a file.
keyStore.setKeyEntry(MONGO_KEY_ENTRY_ALIAS, privateKey, "".toCharArray(), new Certificate[] { certificate });
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "".toCharArray());
return keyManagerFactory.getKeyManagers();
}
/**
* Parses the PEM Encoded <b> mongo.x509.private-key </b> property to a
* {@link PrivateKey}.
*
* @return
*/
@SneakyThrows
private PrivateKey privateKey() {
try (PEMParser parser = new PEMParser(new StringReader(properties.getPrivateKey()))) {
return PEM_KEY_CONVERTER.getPrivateKey(PrivateKeyInfo.class.cast(parser.readObject()));
}
}
/**
* Parses the PEM Encoded <b>mongo.x509.certificate</b> property to
* {@link X509Certificate}.
*
* @return
*/
@SneakyThrows
private X509Certificate x509Certificate() {
try (PEMParser parser = new PEMParser(new StringReader(properties.getCertificate()))) {
return X509_CERTIFICATE_CONVERTER.getCertificate(X509CertificateHolder.class.cast(parser.readObject()));
}
}
}
因此,证书和密钥是通过从 credhub 加载属性的 MongoX509Properties 注入的。
另一个关键是您如何解析证书。为此,我们使用了依赖项:
<!-- Bouncy Castle for parsing PEM encoded private key and certificate -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.65</version>
</dependency>
为了通过验证为 X509 用户使用反应流连接到 mongodb,mongodb 驱动程序强制设置两个 jvm 属性:
javax.net.ssl.keyStore
javax.net.ssl.keyStorePassword
https://mongodb.github.io/mongo-java-driver/4.0/driver-reactive/tutorials/ssl/
我只能设置属性并使其工作在应用程序启动之前
System.setProperty("javax.net.ssl.keyStore", "path");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SpringApplication.run(ChgQuerySvcApplication.class, args);
但是,如果我尝试在扩展 AbstractReactiveMongoConfiguration
的 class 中设置这些属性
它不接听。
@Configuration
public class ReactiveMongoConfiguration extends AbstractReactiveMongoConfiguration {
@Autowired
Environment environment;
@Value("${mypassword}")
private String keyStorePassword;
@Override
public MongoClient reactiveMongoClient() {
MongoProperties properties = new MongoProperties();
properties.setDatabase("somdedb");
String uri = "mongodb+srv://CN=username@clusteraddress/somedb?authSource=%24external&authMechanism=MONGODB-X509&retryWrites=true&w=majority";
properties.setUri(uri);
ReactiveMongoClientFactory factory = new ReactiveMongoClientFactory(properties, environment, null);
System.setProperty("javax.net.ssl.keyStore", "path to key store");
System.setProperty("javax.net.ssl.keyStorePassword", "password"); // possibly replace with keyStorePassword
MongoCredential credential = MongoCredential.createMongoX509Credential("CN=username"); // redundant, I know
MongoClientSettings settings = MongoClientSettings.builder()
.applyToSslSettings(builder -> builder
.applySettings(SslSettings.builder().enabled(true).invalidHostNameAllowed(true).build()))
.credential(credential).build();
return factory.createMongoClient(settings);
}
}
我正在使用的 spring 入门依赖项(版本 2.3.2.RELEASE):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
当我尝试连接时,出现以下异常:
{"@timestamp":"2020-08-07T17:34:18.346-05:00","logger_name":"org.mongodb.driver.client","thread_name":"async-channel-group-0-handler-executor","severity":"ERROR","trace":"","span":"","parent":"","message":"Calling onError threw an exception","stack_trace":"com.mongodb.MongoCommandException: Command failed with error 18 (AuthenticationFailed): 'No verified subject name available from client' on server servername:27017. The full response is {\"operationTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"ok\": 0.0, \"errmsg\": \"No verified subject name available from client\", \"code\": 18, \"codeName\": \"AuthenticationFailed\", \"$clusterTime\": {\"clusterTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"signature\": {\"hash\": {\"$binary\": {\"base64\": \"4IS/JaRasdauyWO9aXVOcaHm2s+3KzKg=\", \"subType\": \"00\"}}, \"keyId\": 123234}}}\r\n\tat com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)\r\n\tat com.mongodb.internal.connection.InternalStreamConnection.onResult(InternalStreamConnection.java:389)\r\n\t... 13 common frames omitted\r\nWrapped by: com.mongodb.MongoSecurityException: Exception authenticating\r\n\tat com.mongodb.internal.connection.X509Authenticator.translat...\r\n"}
{"@timestamp":"2020-08-07T17:34:18.347-05:00","logger_name":"org.mongodb.driver.client","thread_name":"async-channel-group-0-handler-executor","severity":"ERROR","trace":"","span":"","parent":"","message":"Callback onResult call produced an error","stack_trace":"com.mongodb.MongoCommandException: Command failed with error 18 (AuthenticationFailed): 'No verified subject name available from client' on server servername:27017. The full response is {\"operationTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"ok\": 0.0, \"errmsg\": \"No verified subject name available from client\", \"code\": 18, \"codeName\": \"AuthenticationFailed\", \"$clusterTime\": {\"clusterTime\": {\"$timestamp\": {\"t\": 1596839653, \"i\": 1}}, \"signature\": {\"hash\": {\"$binary\": {\"base64\": \"4IS/asdJaRuyaWO9XVOcaHm2s+3KzKg=\", \"subType\": \"00\"}}, \"keyId\": 123234}}}\r\n\tat com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)\r\n\tat com.mongodb.internal.connection.InternalStreamConnection.onResult(InternalStreamConnection.java:389)\r\n\t... 13 common frames omitted\r\nWrapped by: com.mongodb.MongoSecurityException: Exception authenticating\r\n\tat com.mongodb.internal.connection.X509Authenticator.translat...\r\n"}
我尝试这样做的原因是我可以通过 spring 云配置设置密码,而不是硬编码或作为 JVM 参数传递。有没有办法动态设置这些属性?
x.509 身份验证所需的所有选项都应在最新驱动程序的连接字符串中指定。
- 学习connection string documentation.
- 构造一个包含所有选项的连接字符串。
- 使用此连接字符串通过
mongo
shell 连接到您的部署。不要使用 command-line 参数传递任何选项,仅使用连接字符串。 - 使用与您的驱动程序相同的连接字符串。
要解决身份验证错误,请阅读 server log。
tl;dr
在创建 MongoDB 连接之前,我们能够使用 springboot 的 MongoClientSettingsBuilderCustomizer 向 MongoDB 连接工厂提供新的 SSLContext,实质上是覆盖 JVM 中可用的默认 SSLContext
详细解释:
MongoDB 的 Java 驱动程序完全依赖于 JRE 中的 SSLContext
,因此无法
通过连接字符串等设置它。我通过 MongoDB 支持确认了这一点。由于我们利用 spring-boot-starter-data-mongodb-reactive
。我们能够使用 spring-boot 提供的一些定制器。以下是我们的解决方法:
我们创建了定制器的 bean:
@Configuration
@RequiredArgsConstructor
public class MongoX509CredentialClientSettingsBuilderCustomizer implements MongoClientSettingsBuilderCustomizer {
private static final String MONGO_KEY_ENTRY_ALIAS = "mongo-client-key";
private static final JcaX509CertificateConverter X509_CERTIFICATE_CONVERTER = new JcaX509CertificateConverter();
private static final JcaPEMKeyConverter PEM_KEY_CONVERTER = new JcaPEMKeyConverter();
private final MongoX509Properties properties;
/**
* Only customizes the {@link SslSettings} for use with X.509 Certificate
* Authentication
*/
@Override
public void customize(Builder clientSettingsBuilder) {
// @formatter:off
clientSettingsBuilder
.applyToSslSettings(builder -> builder
.applySettings(SslSettings
.builder()
.context(sslContext())
.enabled(true)
.build()))
.credential(MongoCredential.createMongoX509Credential());
// @formatter:on
}
/**
* Creates an {@link SSLContext} that can connect to any endpoint exposing a
* valid well known CA by the JRE. And uses a dynamic array of
* {@link KeyManager} that contains the {@link X509Certificate} and
* {@link PrivateKey} configured for use with a MongoDB instance.
*
* @return
*/
@SneakyThrows
public SSLContext sslContext() {
SSLContext sslContext = SSLContext.getInstance(properties.getTlsVersion());
sslContext.init(keyManagers(x509Certificate(), privateKey()), trustManagers(), null);
return sslContext;
}
/**
* Creates an array of {@link TrustManager} containing the default set of
* trusted certificate authorities. This is required to make a TLS connection to
* the MongoDB instance. If MongoDB is Atlas then the CA is Let's Encrypt and
* should already be trusted so copy that over to the SSLContext that we are
* creating.
*
* @return an array of {@link TrustManager} initialized with the default trust
* managers.
*/
@SneakyThrows
private TrustManager[] trustManagers() {
TrustManagerFactory defaultTrustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// Using null here init the trustManagerFactory with the default trust store.
defaultTrustManagerFactory.init((KeyStore) null);
// only need the default trust managers if the CA for mongo is already in the
// default trust store for the JVM (e.g. via bosh managed trust store or via the
return defaultTrustManagerFactory.getTrustManagers();
}
/**
* Creates an array of {@link KeyManager} containing the certificate and
* privateKey provided in a in memory only {@link KeyStore} with alias
* {@link #MONGO_KEY_ENTRY_ALIAS} used for x509 authentication. This is a dymaic
* key manager containing the private key and certificate in the store for use
* by the {@link SSLContext} that this class creates.
*
* @param certificate
* @param privateKey
* @return an array of {@link KeyManager} initialized with the in memory
* {@link KeyStore}.
*/
@SneakyThrows
private KeyManager[] keyManagers(X509Certificate certificate, PrivateKey privateKey) {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null); // You don't need the KeyStore instance to come from a file.
keyStore.setKeyEntry(MONGO_KEY_ENTRY_ALIAS, privateKey, "".toCharArray(), new Certificate[] { certificate });
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "".toCharArray());
return keyManagerFactory.getKeyManagers();
}
/**
* Parses the PEM Encoded <b> mongo.x509.private-key </b> property to a
* {@link PrivateKey}.
*
* @return
*/
@SneakyThrows
private PrivateKey privateKey() {
try (PEMParser parser = new PEMParser(new StringReader(properties.getPrivateKey()))) {
return PEM_KEY_CONVERTER.getPrivateKey(PrivateKeyInfo.class.cast(parser.readObject()));
}
}
/**
* Parses the PEM Encoded <b>mongo.x509.certificate</b> property to
* {@link X509Certificate}.
*
* @return
*/
@SneakyThrows
private X509Certificate x509Certificate() {
try (PEMParser parser = new PEMParser(new StringReader(properties.getCertificate()))) {
return X509_CERTIFICATE_CONVERTER.getCertificate(X509CertificateHolder.class.cast(parser.readObject()));
}
}
}
因此,证书和密钥是通过从 credhub 加载属性的 MongoX509Properties 注入的。 另一个关键是您如何解析证书。为此,我们使用了依赖项:
<!-- Bouncy Castle for parsing PEM encoded private key and certificate -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.65</version>
</dependency>