英文:
spring-boot-starter-data-mongodb-reactive setting keystore password from application.yml for connecting using X509
问题
要使用响应式流连接到MongoDB并以X509用户进行身份验证,MongoDB驱动程序需要设置两个JVM属性:
javax.net.ssl.keyStore
javax.net.ssl.keyStorePassword
我只能在应用程序启动之前成功设置这些属性,如下所示:
System.setProperty("javax.net.ssl.keyStore", "path");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SpringApplication.run(ChgQuerySvcApplication.class, args);
然而,如果我尝试在扩展AbstractReactiveMongoConfiguration
的类中设置这些属性,它不起作用。
@Configuration
public class ReactiveMongoConfiguration extends AbstractReactiveMongoConfiguration {
@Autowired
Environment environment;
@Value("${mypassword}")
private String keyStorePassword;
@Override
public MongoClient reactiveMongoClient() {
MongoProperties properties = new MongoProperties();
properties.setDatabase("somedb");
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"); // 可能替换为 keyStorePassword
MongoCredential credential = MongoCredential.createMongoX509Credential("CN=username"); // 多余,我知道
MongoClientSettings settings = MongoClientSettings.builder()
.applyToSslSettings(builder -> builder
.applySettings(SslSettings.builder().enabled(true).invalidHostNameAllowed(true).build()))
.credential(credential).build();
return factory.createMongoClient(settings);
}
}
我正在使用的Spring Starter版本是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 {...}\r\n\tat com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)\r\n\t... 13 common frames omitted\r\nWrapped by: com.mongodb.MongoSecurityException: Exception authenticating\r\n\tat com.mongodb.internal.connection.X509Authenticator.translat..."}
{"@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 {...}\r\n\tat com.mongodb.internal.connection.ProtocolHelper.getCommandFailureException(ProtocolHelper.java:175)\r\n\t... 13 common frames omitted\r\nWrapped by: com.mongodb.MongoSecurityException: Exception authenticating\r\n\tat com.mongodb.internal.connection.X509Authenticator.translat..."}
我尝试这样做的原因是,我希望能够通过Spring Cloud Config动态设置密码,而不是硬编码或作为JVM参数传递。是否有一种动态设置这些属性的方法?
英文:
In order to connect to mongodb using reactive streams by authenticating as X509 user, mongo db driver forces to set two jvm properties:
javax.net.ssl.keyStore
javax.net.ssl.keyStorePassword
https://mongodb.github.io/mongo-java-driver/4.0/driver-reactive/tutorials/ssl/
I have only been able to set the properties and make it work before application start
System.setProperty("javax.net.ssl.keyStore", "path");
System.setProperty("javax.net.ssl.keyStorePassword", "password");
SpringApplication.run(ChgQuerySvcApplication.class, args);
However if I try to set those properties in a class that extends AbstractReactiveMongoConfiguration
It doesn't pick up.
@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);
}
}
The spring starter dependency(version 2.3.2.RELEASE) I am using:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
</dependency>
When I try to connect, I get the following exception:
{"@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$2$1.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$2$1.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"}
The reason I am trying to do that is so that I can set the password through spring cloud config instead of hardcoding or passing as JVM Argument. Is there a way to set those properties dynamically ?
答案1
得分: 1
所有与 x.509 认证相关的选项都应该能够在最新的驱动程序中在连接字符串中指定。请参见此处 以查看各种语言中的示例。
- 阅读 连接字符串文档。
- 构建一个包含所有选项的连接字符串。
- 使用此连接字符串连接到您的部署,使用
mongo
shell。不要使用命令行参数传递任何选项,仅使用连接字符串。 - 使用相同的连接字符串与您的驱动程序进行连接。
要排除身份验证错误,请阅读 服务器日志。
英文:
All options required for x.509 authentication should be specifiable in the connection string in recent drivers. See here for examples in various languages.
- Study connection string documentation.
- Construct a connection string containing all of the options.
- Use this connection string to connect to your deployment using
mongo
shell. Do not pass any options using command-line arguments, use connection string only. - Use the same connection string with your driver.
To troubleshoot authentication errors, read the server log.
答案2
得分: 0
我们成功地在创建 MongoDB 连接之前,使用 Spring Boot 的 MongoClientSettingsBuilderCustomizer 提供了一个新的 SSLContext 给 MongoDB 连接工厂,实质上覆盖了 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;
/**
* 只为使用 X.509 证书身份验证自定义 {@link SslSettings}
*/
@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
}
/**
* 创建一个 {@link SSLContext},可以连接到由 JRE 暴露的任何具有有效已知 CA 的终端,并使用包含用于与 MongoDB 实例一起使用的 {@link X509Certificate} 和 {@link PrivateKey} 的动态 {@link KeyManager} 数组。
*
* @return
*/
@SneakyThrows
public SSLContext sslContext() {
SSLContext sslContext = SSLContext.getInstance(properties.getTlsVersion());
sslContext.init(keyManagers(x509Certificate(), privateKey()), trustManagers(), null);
return sslContext;
}
/**
* 创建包含默认一组受信任的证书颁发机构的 {@link TrustManager} 数组。这是与 MongoDB 实例建立 TLS 连接所必需的。如果 MongoDB 是 Atlas,则 CA 是 Let's Encrypt,应该已经受信任,因此将其复制到我们正在创建的 SSLContext 中。
*
* @return 一个包含默认受信任证书颁发机构的 {@link TrustManager} 数组。
*/
@SneakyThrows
private TrustManager[] trustManagers() {
TrustManagerFactory defaultTrustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
// 使用 null 初始化 trustManagerFactory 以使用默认信任库。
defaultTrustManagerFactory.init((KeyStore) null);
// 仅在 MongoDB 的 CA 已在 JVM 的默认信任库中(例如,通过 Bosh 管理的信任库或通过
// 默认信任库中的默认信任库)时才需要默认信任管理器
return defaultTrustManagerFactory.getTrustManagers();
}
/**
* 创建包含在内存中的 {@link KeyStore} 中提供的证书和私钥的 {@link KeyManager} 数组,其别名为 {@link #MONGO_KEY_ENTRY_ALIAS},用于 x509 身份验证。这是一个包含用于 {@link SSLContext} 的私钥和证书的存储库中的动态密钥管理器。
*
* @param certificate
* @param privateKey
* @return 一个使用内存中 {@link KeyStore} 初始化的 {@link KeyManager} 数组。
*/
@SneakyThrows
private KeyManager[] keyManagers(X509Certificate certificate, PrivateKey privateKey) {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null); // 你不需要从文件中获取 KeyStore 实例。
keyStore.setKeyEntry(MONGO_KEY_ENTRY_ALIAS, privateKey, "".toCharArray(), new Certificate[] { certificate });
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "".toCharArray());
return keyManagerFactory.getKeyManagers();
}
/**
* 将 PEM 编码的 <b>mongo.x509.private-key </b> 属性解析为 {@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()));
}
}
/**
* 将 PEM 编码的 <b>mongo.x509.certificate</b> 属性解析为 {@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()));
}
}
}
因此,证书和密钥是通过 MongoX509Properties 注入的,它从 credhub 加载了属性。另一个关键是如何解析证书。为此,我们使用了以下依赖项:
<!-- 用于解析 PEM 编码的私钥和证书的 Bouncy Castle -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.65</version>
</dependency>
英文:
tl;dr
We were able to provide a new SSLContext to MongoDB connection factory using springboot's MongoClientSettingsBuilderCustomizer before creating a MongoDB Connection, essentially overwriting the default SSLContext
Available in JVM
Detailed Explanation:
MongoDB's Java driver relies solely on the SSLContext
in JRE and thus there is no way to
set it through connection string etc. I confirmed this with MongoDB Support. Since we leverage spring-boot-starter-data-mongodb-reactive
. We were able to use some of the Customizers provided by spring-boot. Here is how we solved it:
We created a bean of the customizer:
@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()));
}
}
}
Hence the certificate and key was injected through MongoX509Properties which loaded the properties from a credhub.
Another key to this was how you parse the certificate. For that we used the dependency:
<!-- Bouncy Castle for parsing PEM encoded private key and certificate -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.65</version>
</dependency>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论