April 6, 2026

Writing mTLS/Two-Way SSL/Client Certificate Authentication with Spring Boot 4 and RestClient

Introduction

In my previous I setup the server side with mLTS https://magnus-k-karlsson.blogspot.com/2026/04/configure-mtlstwo-way-sslclient.html

SSL Bundle

application.properties

# https://docs.spring.io/spring-boot/reference/features/ssl.html
spring.ssl.bundle.jks.mybundle.truststore.location=file:src/test/resources/localhost.p12
spring.ssl.bundle.jks.mybundle.truststore.password=changeit

spring.ssl.bundle.jks.mybundle.keystore.location=file:src/test/resources/localhost.p12
spring.ssl.bundle.jks.mybundle.key.alias=localhost
spring.ssl.bundle.jks.mybundle.keystore.password=changeit
spring.ssl.bundle.jks.mybundle.keystore.type=PKCS12

RestClient Configuration

package se.magnuskkarlsson.clientcert;

import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.util.Timeout;
import org.springframework.boot.restclient.autoconfigure.RestClientSsl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestClient;

@Configuration
public class RestClientConfig {

    // https://dev.to/akdevcraft/never-use-spring-restclient-default-implementation-in-production-100g
    // https://docs.spring.io/spring-boot/api/java/org/springframework/boot/restclient/autoconfigure/RestClientSsl.html
    // https://docs.spring.io/spring-boot/reference/features/ssl.html
    // https://docs.spring.io/spring-boot/appendix/application-properties/index.html
    @Bean
    public RestClient restClient(RestClient.Builder restClientBuilder, RestClientSsl ssl) {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(150); // total connections
        connectionManager.setDefaultMaxPerRoute(50); // per-host connections

        RequestConfig requestConfig = RequestConfig.custom() //
                .setConnectTimeout(Timeout.ofMilliseconds(2000)) // time to establish connection (ms)
                .setResponseTimeout(Timeout.ofMilliseconds(3000)) // time waiting for server response (ms)
                .setConnectionRequestTimeout(Timeout.ofMilliseconds(1000)) // time to wait for connection from pool (ms)
                .build();

        CloseableHttpClient httpClient = HttpClients.custom() //
                .setConnectionManager(connectionManager) //
                .setDefaultRequestConfig(requestConfig) //
                .build();

        return restClientBuilder //
                .requestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)) //
                .apply(ssl.fromBundle("mybundle")) //
                .baseUrl("https://localhost:8443") //
                .build();
    }
}

RestClient Code

    @Autowired
    RestClient restClient;

    @Test
    void hello() {
        // https://dzone.com/articles/spring-boot-32-replace-your-resttemplate-with-rest
        var response = restClient.get() //
                .uri("/hello") //
                .retrieve() //
                .toEntity(String.class);
        System.out.println(response.getBody());
    }

No comments: