eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Partner – LambdaTest – NPI EA (cat=Testing)
announcement - icon

Regression testing is an important step in the release process, to ensure that new code doesn't break the existing functionality. As the codebase evolves, we want to run these tests frequently to help catch any issues early on.

The best way to ensure these tests run frequently on an automated basis is, of course, to include them in the CI/CD pipeline. This way, the regression tests will execute automatically whenever we commit code to the repository.

In this tutorial, we'll see how to create regression tests using Selenium, and then include them in our pipeline using GitHub Actions:, to be run on the LambdaTest cloud grid:

>> How to Run Selenium Regression Tests With GitHub Actions

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

eBook – HTTP Client – NPI (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

1. Introduction

In this tutorial, we’ll configure Java’s SSLContext to use different client certificates based on the target server. We’ll start with a straightforward approach using Apache HttpComponents, then implement a custom solution using a routing KeyManager and TrustManager.

2. Scenario and Setup

We’ll simulate a Java client that needs to make HTTPS calls to two different endpoints requiring different client certificates for mutual TLS authentication. For demonstration, we’ll imagine both https://api.service1/ and https://api.service2/ as intranet services.

2.1. Generating the CA, Keys, and Trust Stores

Our setup uses a client/server key pair for each host. Each key is signed by a shared certificate authority (CA), and both sides of the TLS connection trust the CA.

Creating the CA, signed certificates, keystores, and trust stores manually can be error-prone and repetitive. To simplify this, we provide a helper script, automating the process:

  • Create a private certificate authority (CA) and add it to a truststore; for example, trust.api.service1.p12
  • Generate separate key pairs for the client and server
  • Sign both client and server certificates using the CA
  • Package each certificate and key into a PKCS12 keystore for easy loading; for example, client.api.service1.p12 and server.api.service1.p12

Furthermore, we’ll use an alias with the same value as the hostname of each endpoint, which will always be part of each generated file, so it’s easier to distinguish between them by prefix. We also use the same password to simplify the examples. In a real scenario, we’d use different passwords.

2.2. Mocking the Servers and Certificate Setup

We’ll use WireMock to simulate our servers, relying on two properties: CERTS_DIR, with the directory containing the p12 files for both servers, and PASSWORD:

private static WireMockServer mockHttpsServer(String hostname, int port) {
    return new WireMockServer(WireMockConfiguration.options()
      .bindAddress(hostname)
      .httpsPort(port)
      .trustStorePath(CERTS_DIR + "/trust." + host + ".p12")
      .trustStorePassword(password)
      .keystorePath(CERTS_DIR + "/server." + host + ".p12")
      .keystorePassword(PASSWORD)
      .keyManagerPassword(PASSWORD)
      .needClientAuth(true));
}

Let’s go through the most important options:

  • Bind address: Each certificate is bound to a specific hostname, so we specify it here
  • HTTPS port: This is the port we’ll use in our tests
  • Keystore path and trust store path: Since our certificate is self-signed, we also need to specify a trust store. Additionally, to simplify the example, we’re assuming the file names use the prefix “trust.” for the trust store and “server.” for the server key store
  • Client auth: Enabled explicitly for mTLS
  • Passwords: In this example, we’re using the same password

3. Using Apache HTTP Components

Let’s first examine how we can set up different clients with their own SSL contexts using Apache’s HTTP library.

3.1. Configuring the Client

Since we’re assuming the prefix “trust.” for the trust store and “client.” for the client key store, we only need one parameter to receive the endpoint hostname to build our SSLContext. With the help of the library’s SSLContexts, we can load the trust and key material:

private CloseableHttpClient httpsClient(String host) {
    char[] password = PASSWORD.toCharArray();

    SSLContext context = SSLContexts.custom()
      .loadTrustMaterial(Paths.get(CERTS_DIR + "/trust." + host + ".p12"), password)
      .loadKeyMaterial(Paths.get(CERTS_DIR + "/client." + host + ".p12"), password, password)
      .build();

    // ...
}

Then, we create a connection manager and set our newly created SSL context as a TLS socket strategy. We’ll use DefaultClientTlsStrategy, introduced in HttpComponents Core 5:

var manager = PoolingHttpClientConnectionManagerBuilder.create() 
  .setTlsSocketStrategy(new DefaultClientTlsStrategy(context)) 
  .build();

Finally, we use the manager to return a configured HTTPS client, ready to be used to make secure API calls:

return HttpClients.custom()
  .setConnectionManager(manager)
  .build();

3.2. Calling the Endpoints

With the boilerplate ready, we’ll create different client configurations for each API call:

@Test
void whenBuildingSeparateContexts_thenCorrectCertificateUsed() {
    CloseableHttpClient client1 = httpsClient("api.service1");

    HttpGet api1Get = new HttpGet("https://api.service1:10443/test");
    client1.execute(api1Get, response -> {
        assertEquals(HttpStatus.SC_OK, response.getCode());
        return response;
    });

    CloseableHttpClient client2 = httpsClient("api.service2");

    HttpGet api2Get = new HttpGet("https://api.service2:20443/test");
    client2.execute(api2Get, response -> {
        assertEquals(HttpStatus.SC_OK, response.getCode());
        return response;
    });
}

Let’s see what this looks like without third-party dependencies and a single SSLContext.

4. Creating a Custom KeyManager and TrustManager

The X509ExtendedKeyManager and X509ExtendedTrustManager are abstract classes from Java’s SSL package that we can extend to have complete control over how certificate keys and trust stores are loaded and used during SSL handshakes. We’ll use them to create a RoutingSslContextBuilder class that can choose the correct certificate based on the hostname.

4.1. Loading a KeyStore

Let’s start with utilities to load keys and trust managers. We’ll use this method to load a KeyStore into memory:

public class CertUtils {

    private static KeyStore loadKeyStore(Path path, String password) {
        KeyStore store = KeyStore.getInstance(path.toFile(), password.toCharArray());
        try (InputStream stream = Files.newInputStream(path)) {
            store.load(stream, password.toCharArray());
        }
        return store;
    }

    // ...
}

4.2. Loading a KeyManager and a TrustManager

Now let’s put it together to load a key manager of type X509KeyManager. We use this type because KeyManager is only a marker interface, and X.509 is the standard for certificate formats:

public static X509KeyManager loadKeyManager(Path path, String password) {
    KeyStore store = loadKeyStore(path, password);

    KeyManagerFactory factory = 
      KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    factory.init(store, password.toCharArray());

    return (X509KeyManager) Stream.of(factory.getKeyManagers())
      .filter(X509KeyManager.class::isInstance)
      .findAny()
      .orElseThrow();
}

And also a trust manager for the trust store:

public static X509TrustManager loadTrustManager(Path path, String password) {
    KeyStore store = loadKeyStore(path, password);

    TrustManagerFactory factory = 
      TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    factory.init(store);

    return (X509TrustManager) 
      filter(factory.getTrustManagers(), X509TrustManager.class::isInstance);
}

4.3. Creating the RoutingKeyManager

With a custom key manager, we’ll decide which store to use based on the hostname or certificate alias. We’ll do that by storing every keystore in a Map and overriding the methods in X509ExtendedKeyManager.

When extending this class, it’s not always possible to get the hostname. In those cases, we’ll receive the alias in our select() method instead. Therefore, this strategy only works when the hostname of the target server matches the alias of the certificate:

public class RoutingKeyManager extends X509ExtendedKeyManager {

    private final Map<String, X509KeyManager> hostMap = new HashMap<>();

    public void put(String host, X509KeyManager manager) {
        hostMap.put(host, manager);
    }

    private X509KeyManager select(String host) {
        X509KeyManager manager = hostMap.get(host);
        if (manager == null)
            throw new IllegalArgumentException("key manager not found for " + host);

        return manager;
    }

    // ...
}

The chooseEngineClientAlias() method is called to choose an alias for authenticating the client. We get the host from the SSLEngine parameter and delegate the call to its manager’s chooseClientAlias(), ignoring the Socket parameter:

@Override
public String chooseEngineClientAlias(
  String[] keyType, Principal[] issuers, SSLEngine engine) {
    String host = engine.getPeerHost();
    return select(host).chooseClientAlias(keyType, issuers, (Socket) null);
}

Next, we’ll override and delegate getCertificateChain(). Notice that this time we’re selecting the key manager based on the alias:

@Override
public X509Certificate[] getCertificateChain(String alias) {
    return select(alias).getCertificateChain(alias);
}

The last method we need to delegate is getPrivateKey():

@Override
public PrivateKey getPrivateKey(String alias) {
    return select(alias).getPrivateKey(alias);
}

For our purposes, we don’t need the other methods in X509KeyManager, so we’ll throw an UnsupportedOperationException for the remaining overrides:

@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
    throw new UnsupportedOperationException();
}

// ...

4.4. Creating the RoutingTrustManager

Our custom trust manager follows the same idea as our RoutingKeyManager, delegating to one of the registered trust managers based on the hostname:

public class RoutingTrustManager extends X509ExtendedTrustManager {

    private final Map<String, X509TrustManager> hostMap = new HashMap<>();

    public void put(String host, X509TrustManager manager) {
        hostMap.put(host, manager);
    }

    private X509TrustManager select(String host) {
        X509TrustManager manager = hostMap.get(host);
        if (manager == null)
            throw new IllegalArgumentException("trust manager not found for " + host);

        return manager;
    }

    // ...
}

This time, the only implementation we’ll need to care about is the checkServerTrusted() with an SSLEngine parameter:

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
  throws CertificateException {
    String host = engine.getPeerHost();
    select(host).checkServerTrusted(chain, authType);
}

Again, we need to throw UnsupportedOperationExceptions for the other overrides:

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
  throws CertificateException {
    throw new UnsupportedOperationException();
}

5. Putting It All Together to Create a RoutingSslContextBuilder

The final piece is building the SSL context. Since our implementation handles routing internally, we’ll need a single HttpClient for all API calls, even if they require different certificates.

5.1. Creating a Builder

We’ll start with a builder class to combine our custom managers:

public class RoutingSslContextBuilder {

    private final RoutingKeyManager routingKeyManager;
    private final RoutingTrustManager routingTrustManager;

    public RoutingSslContextBuilder() {
        routingKeyManager = new RoutingKeyManager();
        routingTrustManager = new RoutingTrustManager();
    }

    public static RoutingSslContextBuilder create() {
        return new RoutingSslContextBuilder();
    }

    // ...
}

When building an instance, we’ll call this method for every combination of host and certificate. This loads both the key and trust manager for every server we need to access:

public RoutingSslContextBuilder trust(String host, String certsDir, String password) {
    routingTrustManager.put(host, CertUtils.loadTrustManager(
      Paths.get(certsDir, "trust." + host + ".p12"), password));
    routingKeyManager.put(host, CertUtils.loadKeyManager(
      Paths.get(certsDir, "client." + host + ".p12"), password));
    return this;
}

Lastly, we’ll initialize the SSL context with our custom managers, leaving the SecureRandom parameter null to get the default implementation:

public SSLContext build() throws NoSuchAlgorithmException, KeyManagementException {
    SSLContext context = SSLContext.getInstance("TLS");
    context.init(
      new KeyManager[] { routingKeyManager }, 
      new TrustManager[] { routingTrustManager }, 
      null);

    return context;
}

5.2. Testing With Java’s HttpClient

Since we’re not dependent on the Apache HttpComponents library, we’ll use core Java to make the calls. Let’s start by building the SSL context and an HttpClient:

@Test
void whenBuildingCustomSslContext_thenCorrectCertificateUsedForEachConnection() {
    SSLContext context = RoutingSslContextBuilder.create()
      .trust("api.service1", CERTS_DIR, PASSWORD)
      .trust("api.service2", CERTS_DIR, PASSWORD)
      .build();

    HttpClient client = HttpClient.newBuilder()
      .sslContext(context)
      .build();

    // ...
}

Now, let’s make the first call:

HttpRequest request = HttpRequest.newBuilder()
  .uri(URI.create("https://api.service1:10443/test"))
  .GET()
  .build();

HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

assertEquals("ok from server 1", response.body());

Then, the second call:

request = HttpRequest.newBuilder()
  .uri(URI.create("https://api.service2:20443/test"))
  .GET()
  .build();

response = client.send(request, HttpResponse.BodyHandlers.ofString());

assertEquals("ok from server 2", response.body());

As expected, we were able to make secure requests to different servers that require different certificates using the same SSL context.

6. Conclusion

In this article, we demonstrated how to use multiple client certificates in Java when interacting with different HTTPS endpoints. We started with a multi-client solution using Apache HttpComponents and then built a custom solution with core Java, using custom KeyManager and TrustManager implementations. This approach is especially useful in systems requiring dynamic TLS credential selection, such as service meshes, multitenant clients, or API gateways.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

Course – LS – NPI (cat=HTTP Client-Side)
announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Course – LSS – NPI (cat=Security/Spring Security)
announcement - icon

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security:

>> CHECK OUT THE COURSE

eBook Jackson – NPI EA – 3 (cat = Jackson)