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 身份验证所需的所有选项都应在最新驱动程序的连接字符串中指定。 各种语言的示例。

  1. 学习connection string documentation.
  2. 构造一个包含所有选项的连接字符串。
  3. 使用此连接字符串通过 mongo shell 连接到您的部署。不要使用 command-line 参数传递任何选项,仅使用连接字符串。
  4. 使用与您的驱动程序相同的连接字符串。

要解决身份验证错误,请阅读 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>