Add SSL service connection support for AMQP

See gh-41137
This commit is contained in:
Moritz Halbritter 2025-02-11 10:17:59 +01:00
parent b62a0c1ae0
commit e26ccbe028
9 changed files with 121 additions and 51 deletions

View File

@ -48,7 +48,7 @@ public abstract class AbstractConnectionFactoryConfigurer<T extends AbstractConn
* @param properties the properties to use to configure the connection factory
*/
protected AbstractConnectionFactoryConfigurer(RabbitProperties properties) {
this(properties, new PropertiesRabbitConnectionDetails(properties));
this(properties, new PropertiesRabbitConnectionDetails(properties, null));
}
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -38,7 +38,7 @@ public class CachingConnectionFactoryConfigurer extends AbstractConnectionFactor
* @param properties the properties to use to configure the connection factory
*/
public CachingConnectionFactoryConfigurer(RabbitProperties properties) {
this(properties, new PropertiesRabbitConnectionDetails(properties));
this(properties, new PropertiesRabbitConnectionDetails(properties, null));
}
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,12 @@ package org.springframework.boot.autoconfigure.amqp;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties.Ssl;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Adapts {@link RabbitProperties} to {@link RabbitConnectionDetails}.
*
@ -30,8 +36,11 @@ class PropertiesRabbitConnectionDetails implements RabbitConnectionDetails {
private final RabbitProperties properties;
PropertiesRabbitConnectionDetails(RabbitProperties properties) {
private final SslBundles sslBundles;
PropertiesRabbitConnectionDetails(RabbitProperties properties, SslBundles sslBundles) {
this.properties = properties;
this.sslBundles = sslBundles;
}
@Override
@ -61,4 +70,17 @@ class PropertiesRabbitConnectionDetails implements RabbitConnectionDetails {
return addresses;
}
@Override
public SslBundle getSslBundle() {
Ssl ssl = this.properties.getSsl();
if (!ssl.determineEnabled()) {
return null;
}
if (StringUtils.hasLength(ssl.getBundle())) {
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
return this.sslBundles.getBundle(ssl.getBundle());
}
return null;
}
}

View File

@ -90,18 +90,17 @@ public class RabbitAutoConfiguration {
@Bean
@ConditionalOnMissingBean
RabbitConnectionDetails rabbitConnectionDetails() {
return new PropertiesRabbitConnectionDetails(this.properties);
RabbitConnectionDetails rabbitConnectionDetails(ObjectProvider<SslBundles> sslBundles) {
return new PropertiesRabbitConnectionDetails(this.properties, sslBundles.getIfAvailable());
}
@Bean
@ConditionalOnMissingBean
RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader,
RabbitConnectionDetails connectionDetails, ObjectProvider<CredentialsProvider> credentialsProvider,
ObjectProvider<CredentialsRefreshService> credentialsRefreshService,
ObjectProvider<SslBundles> sslBundles) {
ObjectProvider<CredentialsRefreshService> credentialsRefreshService) {
RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader,
this.properties, connectionDetails, sslBundles.getIfAvailable());
this.properties, connectionDetails);
configurer.setCredentialsProvider(credentialsProvider.getIfUnique());
configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique());
return configurer;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.amqp;
import java.util.List;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.util.Assert;
/**
@ -73,6 +74,15 @@ public interface RabbitConnectionDetails extends ConnectionDetails {
return addresses.get(0);
}
/**
* SSL bundle to use.
* @return the SSL bundle to use
* @since 3.5.0
*/
default SslBundle getSslBundle() {
return null;
}
/**
* A RabbitMQ address.
*

View File

@ -48,8 +48,6 @@ public class RabbitConnectionFactoryBeanConfigurer {
private final RabbitConnectionDetails connectionDetails;
private final SslBundles sslBundles;
private CredentialsProvider credentialsProvider;
private CredentialsRefreshService credentialsRefreshService;
@ -61,7 +59,7 @@ public class RabbitConnectionFactoryBeanConfigurer {
* @param properties the properties
*/
public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties) {
this(resourceLoader, properties, new PropertiesRabbitConnectionDetails(properties));
this(resourceLoader, properties, new PropertiesRabbitConnectionDetails(properties, null));
}
/**
@ -96,7 +94,6 @@ public class RabbitConnectionFactoryBeanConfigurer {
this.resourceLoader = resourceLoader;
this.rabbitProperties = properties;
this.connectionDetails = connectionDetails;
this.sslBundles = sslBundles;
}
public void setCredentialsProvider(CredentialsProvider credentialsProvider) {
@ -129,16 +126,14 @@ public class RabbitConnectionFactoryBeanConfigurer {
.asInt(Duration::getSeconds)
.to(factory::setRequestedHeartbeat);
map.from(this.rabbitProperties::getRequestedChannelMax).to(factory::setRequestedChannelMax);
RabbitProperties.Ssl ssl = this.rabbitProperties.getSsl();
if (ssl.determineEnabled()) {
factory.setUseSSL(true);
if (ssl.getBundle() != null) {
SslBundle bundle = this.sslBundles.getBundle(ssl.getBundle());
if (factory instanceof SslBundleRabbitConnectionFactoryBean sslFactory) {
sslFactory.setSslBundle(bundle);
}
}
else {
SslBundle sslBundle = this.connectionDetails.getSslBundle();
if (sslBundle != null) {
applySslBundle(factory, sslBundle);
}
else {
RabbitProperties.Ssl ssl = this.rabbitProperties.getSsl();
if (ssl.determineEnabled()) {
factory.setUseSSL(true);
map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm);
map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType);
map.from(ssl::getKeyStore).to(factory::setKeyStore);
@ -148,10 +143,10 @@ public class RabbitConnectionFactoryBeanConfigurer {
map.from(ssl::getTrustStore).to(factory::setTrustStore);
map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase);
map.from(ssl::getTrustStoreAlgorithm).whenNonNull().to(factory::setTrustStoreAlgorithm);
map.from(ssl::isValidateServerCertificate)
.to((validate) -> factory.setSkipServerCertificateValidation(!validate));
map.from(ssl::isVerifyHostname).to(factory::setEnableHostnameVerification);
}
map.from(ssl::isValidateServerCertificate)
.to((validate) -> factory.setSkipServerCertificateValidation(!validate));
map.from(ssl::isVerifyHostname).to(factory::setEnableHostnameVerification);
}
map.from(this.rabbitProperties::getConnectionTimeout)
.whenNonNull()
@ -169,4 +164,11 @@ public class RabbitConnectionFactoryBeanConfigurer {
.to(factory::setMaxInboundMessageBodySize);
}
private static void applySslBundle(RabbitConnectionFactoryBean factory, SslBundle bundle) {
factory.setUseSSL(true);
if (factory instanceof SslBundleRabbitConnectionFactoryBean sslFactory) {
sslFactory.setSslBundle(bundle);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,11 +18,15 @@ package org.springframework.boot.autoconfigure.amqp;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundle;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link PropertiesRabbitConnectionDetails}.
@ -33,13 +37,24 @@ class PropertiesRabbitConnectionDetailsTests {
private static final int DEFAULT_PORT = 5672;
private DefaultSslBundleRegistry sslBundleRegistry;
private RabbitProperties properties;
private PropertiesRabbitConnectionDetails propertiesRabbitConnectionDetails;
@BeforeEach
void setUp() {
this.properties = new RabbitProperties();
this.sslBundleRegistry = new DefaultSslBundleRegistry();
this.propertiesRabbitConnectionDetails = new PropertiesRabbitConnectionDetails(this.properties,
this.sslBundleRegistry);
}
@Test
void getAddresses() {
RabbitProperties properties = new RabbitProperties();
properties.setAddresses(List.of("localhost", "localhost:1234", "[::1]", "[::1]:32863"));
PropertiesRabbitConnectionDetails propertiesRabbitConnectionDetails = new PropertiesRabbitConnectionDetails(
properties);
List<Address> addresses = propertiesRabbitConnectionDetails.getAddresses();
this.properties.setAddresses(List.of("localhost", "localhost:1234", "[::1]", "[::1]:32863"));
List<Address> addresses = this.propertiesRabbitConnectionDetails.getAddresses();
assertThat(addresses.size()).isEqualTo(4);
assertThat(addresses.get(0).host()).isEqualTo("localhost");
assertThat(addresses.get(0).port()).isEqualTo(DEFAULT_PORT);
@ -51,4 +66,27 @@ class PropertiesRabbitConnectionDetailsTests {
assertThat(addresses.get(3).port()).isEqualTo(32863);
}
@Test
void shouldReturnSslBundle() {
SslBundle bundle1 = mock(SslBundle.class);
this.sslBundleRegistry.registerBundle("bundle-1", bundle1);
this.properties.getSsl().setBundle("bundle-1");
SslBundle sslBundle = this.propertiesRabbitConnectionDetails.getSslBundle();
assertThat(sslBundle).isSameAs(bundle1);
}
@Test
void shouldReturnNullIfSslIsEnabledButBundleNotSet() {
this.properties.getSsl().setEnabled(true);
SslBundle sslBundle = this.propertiesRabbitConnectionDetails.getSslBundle();
assertThat(sslBundle).isNull();
}
@Test
void shouldReturnNullIfSslIsNotEnabled() {
this.properties.getSsl().setEnabled(false);
SslBundle sslBundle = this.propertiesRabbitConnectionDetails.getSslBundle();
assertThat(sslBundle).isNull();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import java.util.List;
import org.testcontainers.containers.RabbitMQContainer;
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
@ -66,10 +67,15 @@ class RabbitContainerConnectionDetailsFactory
@Override
public List<Address> getAddresses() {
URI uri = URI.create(getContainer().getAmqpUrl());
URI uri = URI.create((getSslBundle() != null) ? getContainer().getAmqpsUrl() : getContainer().getAmqpUrl());
return List.of(new Address(uri.getHost(), uri.getPort()));
}
@Override
public SslBundle getSslBundle() {
return super.getSslBundle();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,9 +28,10 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.testcontainers.service.connection.PemKeyStore;
import org.springframework.boot.testcontainers.service.connection.PemTrustStore;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@ -39,25 +40,17 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Scott Frederick
*/
@SpringBootTest(properties = { "spring.rabbitmq.ssl.bundle=client",
"spring.ssl.bundle.pem.client.keystore.certificate=classpath:ssl/test-client.crt",
"spring.ssl.bundle.pem.client.keystore.private-key=classpath:ssl/test-client.key",
"spring.ssl.bundle.pem.client.truststore.certificate=classpath:ssl/test-ca.crt" })
@SpringBootTest
@Testcontainers(disabledWithoutDocker = true)
@ExtendWith(OutputCaptureExtension.class)
class SampleAmqpSimpleApplicationSslTests {
@Container
@ServiceConnection
@PemKeyStore(certificate = "classpath:ssl/test-client.crt", privateKey = "classpath:ssl/test-client.key")
@PemTrustStore("classpath:ssl/test-ca.crt")
static final SecureRabbitMqContainer rabbit = TestImage.container(SecureRabbitMqContainer.class);
@DynamicPropertySource
static void secureRabbitMqProperties(DynamicPropertyRegistry registry) {
registry.add("spring.rabbitmq.host", rabbit::getHost);
registry.add("spring.rabbitmq.port", rabbit::getAmqpsPort);
registry.add("spring.rabbitmq.username", rabbit::getAdminUsername);
registry.add("spring.rabbitmq.password", rabbit::getAdminPassword);
}
@Autowired
private Sender sender;