Solution for Spring Boot 2
The following answer is aimed at folk developing against Spring Boot 2 and using self-signed certificates for development (proper certificates recommended for production - see https://letsencrypt.org/).
You can create a keystore file containing self-signed certs using the keytool
command: -
keytool -genkey -storetype PKCS12 \
-alias selfsigned_localhost_sslserver \
-keyalg RSA -keysize 2048 -validity 3650 \
-dname "CN=localhost, OU=Engineering, O=Acme Corp, L=New York, S=New York, C=US" \
-noprompt -keypass changeit -storepass changeit \
-keystore keystore-self-signed.p12
The keystore-self-signed.p12
file will contain a self-signed certificate and this file can be moved into the src/main/resources
folder (or src/test/resources
if you prefer).
Add the following to your application.yaml
Spring config to use SSL and point to the keystore: -
server:
port: 443
ssl:
enabled: true
key-store: classpath:keystore-self-signed.p12
key-store-type: PKCS12
protocol: TLS
enabled-protocols: TLSv1.2 # Best practice - see https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices
key-password: changeit
key-store-password: changeit
Let's create a super simple Spring Boot controller endpoint to test: -
@RestController
public class PingController {
@GetMapping("/ping")
public ResponseEntity<String> ping() {
return new ResponseEntity<>("pong", HttpStatus.OK);
}
}
We can now hit this endpoint with a curl
command (or Postman) i.e.
$ curl https://localhost/ping --insecure --silent
pong
Note: if we don't include --insecure
then curl
will return curl: (60) SSL certificate problem: self signed certificate
.
To test a proper Spring Boot integration test to his endpoint using TestRestTemplate
then we can do the following: -
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class PingControllerTest {
@Value("${server.ssl.key-store}")
private Resource keyStore; // inject keystore specified in config
@Value("${server.ssl.key-store-password}")
private String keyStorePassword; // inject password from config
@LocalServerPort
protected int port; // server port picked randomly at runtime
private TestRestTemplate restTemplate;
@Before
public void setup() throws Exception {
SSLContext sslContext = new SSLContextBuilder()
.loadTrustMaterial(
keyStore.getURL(),
keyStorePassword.toCharArray()
).build();
SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);
HttpClient httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(
httpClient);
RestTemplateBuilder rtb = new RestTemplateBuilder()
.requestFactory(() -> factory)
.rootUri("https://localhost:" + port);
this.restTemplate = new TestRestTemplate(rtb, null, null, HttpClientOption.SSL);
}
@Test
public void shouldPing() {
ResponseEntity<String> result = restTemplate.getForEntity("/ping", String.class);
assertEquals(HttpStatus.OK, result.getStatusCode());
assertEquals("pong", result.getBody());
}
}
As you can see the setup
method creates an instance of the SSLContext
object which loads (and "trusts") the self-sign certs in the keystore-self-signed.p12
file (injected via the Spring Resource
object).
The SSLContext
class is injected into a SSLConnectionSocketFactory
object, which in turn is injected into a HttpClient
object which is then injected into a HttpComponentsClientHttpRequestFactory
object.
This factory object is finally injected into a TestRestTemplate
instance for use in the shouldPing
integration test.
NOTE - I initially lost time with the following code:
...
this.restTemplate = new TestRestTemplate(rgb);
... but this returned ...
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://localhost:56976/ping":
sun.security.validator.ValidatorException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target; nested exception is
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
After debugging through the TestRestTemplate
I realised that you must use the 4 parameter constructor of TestRestTemplate
with HttpClientOption.SSL
i.e.
this.restTemplate = new TestRestTemplate(rtb, null, null, HttpClientOption.SSL);
However, if you're using normal RestTemplate
(e.g. outside of Spring tests) then
the following works: -
...
RestTemplate restTemplate = new RestTemplate(rgb);
NOTE, to improve - create a @Bean
method which returns a TestRestTemplate
instance.