Embedded ActiveMQ with SSL

By | August 15, 2016

In this post I will show how to run an embedded instance of ActiveMQ that uses SSL for communication. In the process I will also show how to configure a JMS client to use SSL.
The resulting project is available on GitHub.

I have omitted the keystore and truststore in this article – if you want to use the exact ones I have used in the example project, they are available in the GitHub repository.

Maven pom.xml File

The example project is a Maven project with  the following pom.xml file.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>se.ivankrizsan.activemq</groupId>
    <artifactId>embedded-activemq-ssl</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <spring.version>4.3.2.RELEASE</spring.version>
        <activemq.version>5.14.0</activemq.version>
        <junit.version>4.12</junit.version>
        <log4j.version>2.6.2</log4j.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.apache.activemq</groupId>
            <artifactId>activemq-spring</artifactId>
            <version>${activemq.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jms</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- Logging dependencies. -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>${log4j.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>${log4j.version}</version>
        </dependency>

        <!-- Test dependencies. -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Nothing fancy in here; Spring is used for the unit-test and for the JMS client, the unit-tests are implemented using JUnit4. Note that Java 8 is used.

Properties File

I am using a properties file to be able to easily configure the different aspects of JMS communication, such as keystore and truststore, JMS broker URL and so on. Note that there are two different JMS broker URLs; one used by the client and one by the embedded ActiveMQ broker. The reason for this is to make it easy to check whether the client can connect to the broker using a TCP connection, without SSL. I would not expect there being a need for two URL properties in a regular application. The default is for the client to use the SSL protocol.

# URL on which the (embedded) broker will expose a connector.
JMS_BROKER_URL=ssl://localhost:61616
# Truststore used by the JMS broker and clients.
JMS_BROKER_TRUSTSTORE=certs/truststore.jks
JMS_BROKER_TRUSTSTORE_TYPE=JKS
JMS_BROKER_TRUSTSTORE_PASSWORD=secret
# Keystore used by JMS broker and clients.
JMS_BROKER_KEYSTORE=certs/clientkeystore.jks
JMS_BROKER_KEYSTORE_TYPE=JKS
JMS_BROKER_KEYSTORE_PASSWORD=secret

# JMS broker URL used by the client to connect to the JMS broker.
CLIENT_JMS_BROKER_URL=ssl://localhost:61616

Read Properties Configuration

Since I want to be able to inject values from the property file above into my test class, as we will shortly see, I need some Spring configuration:

package se.ivankrizsan.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;

/**
 * Configuration that enables injection of configuration property values in Spring-based tests.
 *
 * @author Ivan Krizsan
 */
@Configuration
public class ReadTestPropertiesConfiguration {
    /**
     * Bean that enables injection of configuration property values.
     *
     * @return PropertySourcesPlaceholderConfigurer bean.
     */
    @Bean
    public static PropertySourcesPlaceholderConfigurer testProperties() {
        final PropertySourcesPlaceholderConfigurer theConfigurer = new PropertySourcesPlaceholderConfigurer();
        theConfigurer.setLocation(new ClassPathResource("embedded-activemq-ssl.properties"));
        return theConfigurer;
    }
}

Note that the method annotated with the @Bean annotation is static. The reason for this is to avoid the risk for lifecycle conflicts, as described in the @Bean annotation JavaDoc, under the Bootstrapping section.

JMS Configuration

The following class contains the JMS related Spring beans as well as the bean that starts the embedded ActiveMQ instance for the test.package se.ivankrizsan.configuration;

import org.apache.activemq.ActiveMQConnectionFactory;
import org.apache.activemq.ActiveMQSslConnectionFactory;
import org.apache.activemq.broker.BrokerService;
import org.apache.activemq.broker.SslBrokerService;
import org.apache.activemq.usage.MemoryUsage;
import org.apache.activemq.usage.SystemUsage;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jms.core.JmsTemplate;

import javax.jms.ConnectionFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.security.KeyStore;

/**
 * Configuration of embedded ActiveMQ broker and related helpers used in tests.
 *
 * @author Ivan Krizsan
 */
@Configuration
public class JmsTestConfiguration {
    /* Constant(s): */

    /* Configuration parameter(s): */
    @Value("${JMS_BROKER_URL}")
    protected String mJmsBrokerUrl;
    @Value("${JMS_BROKER_TRUSTSTORE}")
    protected String mJmsBrokerTruststore;
    @Value("${JMS_BROKER_TRUSTSTORE_TYPE}")
    protected String mJmsBrokerTruststoreType;
    @Value("${JMS_BROKER_TRUSTSTORE_PASSWORD}")
    protected String mJmsBrokerTruststorePassword;
    @Value("${JMS_BROKER_KEYSTORE}")
    protected String mJmsBrokerKeystore;
    @Value("${JMS_BROKER_KEYSTORE_TYPE}")
    protected String mJmsBrokerKeystoreType;
    @Value("${JMS_BROKER_KEYSTORE_PASSWORD}")
    protected String mJmsBrokerKeystorePassword;
    @Value(("${CLIENT_JMS_BROKER_URL}"))
    protected String mClientJmsBrokerUrl;

    /**
     * JMS template for tests.
     *
     * @return JMS template.
     */
    @Bean
    public JmsTemplate jmsTestTemplate(final ConnectionFactory inConnectionFactory) {
        final JmsTemplate theJmsTestTemplate = new JmsTemplate(inConnectionFactory);
        theJmsTestTemplate.setReceiveTimeout(5000L);
        return theJmsTestTemplate;
    }

    /**
     * Client JMS connection factory.
     * Depending on the client JMS broker URL, a TCP or a SSL connection factory will be created.
     *
     * @return JMS connection factory.
     */
    @Bean
    @DependsOn("embeddedBroker")
    public ConnectionFactory clientJMSConnectionFactory() {
        final ActiveMQConnectionFactory theConnectionFactory;

        if (mClientJmsBrokerUrl.startsWith("ssl")) {
            final ActiveMQSslConnectionFactory theSSLConnectionFactory =
                new ActiveMQSslConnectionFactory(mClientJmsBrokerUrl);
            try {
                theSSLConnectionFactory.setTrustStore(mJmsBrokerTruststore);
                theSSLConnectionFactory.setTrustStorePassword(mJmsBrokerTruststorePassword);
                theSSLConnectionFactory.setKeyStore(mJmsBrokerKeystore);
                theSSLConnectionFactory.setKeyStorePassword(mJmsBrokerKeystorePassword);

                theConnectionFactory = theSSLConnectionFactory;
            } catch (final Exception theException) {
                throw new Error(theException);
            }
        } else {
            theConnectionFactory = new ActiveMQConnectionFactory(mClientJmsBrokerUrl);
        }

        return theConnectionFactory;
    }

    /**
     * Embedded ActiveMQ broker for tests.
     * The embedded broker exposes only a SSL connector for clients.
     *
     * @return Embedded ActiveMQ broker.
     */
    @Bean(initMethod = "start", destroyMethod = "stop")
    public BrokerService embeddedBroker() {
        SslBrokerService theActiveMqBroker;

        try {
            theActiveMqBroker = new SslBrokerService();
            theActiveMqBroker.setUseJmx(false);
            theActiveMqBroker.setPersistent(false);
            theActiveMqBroker.setUseShutdownHook(true);

            /* Add ActiveMQ SSL connector using configured keystore and truststore. */
            final KeyManager[] theKeystore = readKeystore();
            final TrustManager[] theTruststore = readTruststore();
            theActiveMqBroker.addSslConnector(mJmsBrokerUrl + "?transport.needClientAuth=true",
                theKeystore,
                theTruststore,
                null);

            /* Set memory limit in order not to use too much memory during tests. */
            final MemoryUsage theActiveMqMemoryUsage = new MemoryUsage();
            theActiveMqMemoryUsage.setPercentOfJvmHeap(20);
            final SystemUsage theActiveMqSystemUsage = new SystemUsage();
            theActiveMqSystemUsage.setMemoryUsage(theActiveMqMemoryUsage);
            theActiveMqBroker.setSystemUsage(theActiveMqSystemUsage);
        } catch (final Exception theException) {
            throw new Error("An error occurred starting test ActiveMQ broker", theException);
        }

        return theActiveMqBroker;
    }

    private KeyManager[] readKeystore() throws Exception {
        final KeyManagerFactory
            theKeyManagerFactory
            = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        final KeyStore theKeyStore = KeyStore.getInstance(mJmsBrokerKeystoreType);

        final Resource theKeystoreResource = new ClassPathResource(mJmsBrokerKeystore);
        theKeyStore.load(theKeystoreResource.getInputStream(), mJmsBrokerKeystorePassword.toCharArray());
        theKeyManagerFactory.init(theKeyStore, mJmsBrokerKeystorePassword.toCharArray());
        final KeyManager[] theKeystoreManagers = theKeyManagerFactory.getKeyManagers();
        return theKeystoreManagers;
    }

    private TrustManager[] readTruststore() throws Exception {
        final KeyStore theTruststore = KeyStore.getInstance(mJmsBrokerTruststoreType);

        final Resource theTruststoreResource = new ClassPathResource(mJmsBrokerTruststore);
        theTruststore.load(theTruststoreResource.getInputStream(), null);
        final TrustManagerFactory theTrustManagerFactory
            = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
        theTrustManagerFactory.init(theTruststore);
        final TrustManager[] theTrustManagers = theTrustManagerFactory.getTrustManagers();
        return theTrustManagers;
    }
}

Note that the clientJMSConnectionFactory method creates different types of ActiveMQ connection factory depending on whether the client broker URL starts with “ssl” or not. For SSL communication, a keystore and truststore is configured on the connection factory. The JmsTemplate used by the client to send and receive messages does not require any special configuration to use SSL or TCP apart from the ActiveMQ connection factory – everything is encapsulated in the connection factory.
In addition, note how property values from the properties-file are injected using the @Value annotation.

Test Class

With the dependencies and all the configuration in place, we are now ready to implement the test. The test attempts to send a message to a queue on the broker and later receive the same message from the same queue.

package se.ivankrizsan.activemq;

import org.apache.activemq.command.ActiveMQTextMessage;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import se.ivankrizsan.configuration.JmsTestConfiguration;
import se.ivankrizsan.configuration.ReadTestPropertiesConfiguration;

import javax.jms.Message;
import javax.jms.TextMessage;

/**
 * Tests connecting to the embedded ActiveMQ broker using SSL connections.
 *
 * @author Ivan Krizsan
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { ReadTestPropertiesConfiguration.class, JmsTestConfiguration.class })
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
public class EmbeddedActiveMQSSLTest {
    /** Class logger. */
    private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedActiveMQSSLTest.class);

    /* Constant(s): */
    protected final static String TEST_QUEUE = "testQueue";
    protected final static String TEST_MESSAGE_STRING = "This is a text message!";

    /* Instance variable(s): */
    @Autowired
    protected JmsTemplate mJmsTestTemplate;

    /**
     * Tests sending and receiving a message to/from the broker.
     * Expected result:
     * It should be possible to send and receive one message to/from the JMS broker.
     * The payload of the received message should be the same as the payload of the sent message.
     *
     * @throws Exception If error occurs. Indicates test failure.
     */
    @Test
    public void testSendAndReceiver() throws Exception {
        LOGGER.debug("About to send JMS message");
        mJmsTestTemplate.send(TEST_QUEUE,
            (inSession) -> {
                final TextMessage theTextMessage = new ActiveMQTextMessage();
                theTextMessage.setText(TEST_MESSAGE_STRING);
                return theTextMessage;
            });
        LOGGER.debug("JMS message sent");

        LOGGER.debug("About to receive JMS message");
        final Message theMessage = mJmsTestTemplate.receive(TEST_QUEUE);
        LOGGER.debug("JMS message received");

        Assert.assertNotNull("There should be a message on the queue", theMessage);
        Assert.assertTrue("The message should be a text message", theMessage instanceof  TextMessage);
        final TextMessage theTextMessage = (TextMessage)theMessage;
        LOGGER.debug("Contents of received message: {}", theTextMessage.getText());
        Assert.assertEquals("Message contents should match", TEST_MESSAGE_STRING, theTextMessage.getText());
    }
}

I would not print log in regular tests, but since this is an educational example there is some logging in this test class. The Spring JmsTemplate is used to send and receive a message.

Log4J Configuration File

For completeness, I will list the Log4J configuration file here.

<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
    <Appenders>
        <Console name="STDOUT" target="SYSTEM_OUT">
            <PatternLayout pattern="%d %-5p %-30C - %m%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="se.ivankrizsan" level="debug"/>
        <Logger name="org.springframework" level="info"/>
        <Root level="info">
            <AppenderRef ref="STDOUT"/>
        </Root>
    </Loggers>
</Configuration>

Running the Example

If we now run the test, output similar to the following should be output to the console:

INFO: Loading properties file from class path resource [embedded-activemq-ssl.properties]
2016-08-15 08:26:02,280 INFO  org.apache.activemq.broker.BrokerService - Using Persistence Adapter: MemoryPersistenceAdapter
2016-08-15 08:26:02,413 INFO  org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) is starting
2016-08-15 08:26:02,417 INFO  org.apache.activemq.transport.TransportServerThreadSupport - Listening for connections at: ssl://localhost:61616?transport.needClientAuth=true
2016-08-15 08:26:02,418 INFO  org.apache.activemq.broker.TransportConnector - Connector ssl://localhost:61616?transport.needClientAuth=true started
2016-08-15 08:26:02,418 INFO  org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) started
2016-08-15 08:26:02,418 INFO  org.apache.activemq.broker.BrokerService - For help or more information please see: http://activemq.apache.org2016-08-15 08:26:02,529 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - About to send JMS message
2016-08-15 08:26:02,669 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - JMS message sent
2016-08-15 08:26:02,670 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - About to receive JMS message
2016-08-15 08:26:02,708 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - JMS message received
2016-08-15 08:26:02,709 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - Contents of received message: This is a text message!
2016-08-15 08:26:02,711 INFO  org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) is shutting down
2016-08-15 08:26:02,711 INFO  org.apache.activemq.broker.TransportConnector - Connector ssl://localhost:61616?transport.needClientAuth=true stopped
Aug 15, 2016 8:26:02 AM org.springframework.context.support.GenericApplicationContext doClose
INFO: Closing org.springframework.context.support.GenericApplicationContext@9353778: startup date [Mon Aug 15 08:26:01 CEST 2016]; root of context hierarchy
2016-08-15 08:26:02,716 INFO  org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) uptime 0.444 seconds
2016-08-15 08:26:02,717 INFO  org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) is shutdown
Process finished with exit code 0

The test should pass and we can see from the log that a JMS message was sent and a JMS message was received, as expected.

If we now modify the property CLIENT_JMS_BROKER_URL in the properties file to use plain TCP, like this:

CLIENT_JMS_BROKER_URL=tcp://localhost:61616

Then we re-run the test, which should now fail:

2016-08-15 11:39:49,935 ERROR org.apache.activemq.broker.TransportConnector$1 - Could not accept connection from tcp://127.0.0.1:53050 : javax.net.ssl.SSLException: Unsupported record version Unknown-0.1
Aug 15, 2016 11:39:49 AM org.springframework.context.support.GenericApplicationContext doClose
INFO: Closing org.springframework.context.support.GenericApplicationContext@9353778: startup date [Mon Aug 15 11:39:49 CEST 2016]; root of context hierarchy

org.springframework.jms.UncategorizedJmsException: Uncategorized exception occurred during JMS processing; nested exception is javax.jms.JMSException: Cannot send, channel has already failed: tcp://127.0.0.1:61616

Thus we have created an embedded ActiveMQ broker which only accepts connections over SSL.

Happy coding!

One thought on “Embedded ActiveMQ with SSL

Leave a Reply

Your email address will not be published. Required fields are marked *