1. Introduction
In all the examples to date (and likely forward), we have been using the HTTP protocol. This has been very easy option to use, but I likely do not have to tell you that straight HTTP is NOT secure for use and especially NOT appropriate for use with credentials or any other authenticated information.
Hypertext Transfer Protocol Secure (HTTPS) — with trusted certificates — is the secure way to communicate using APIs in modern environments. We still will want the option of simple HTTP in development and most deployment environments provide an external HTTPS proxy that can take care of secure communications with the external clients. However, it will be good to take a short look at how we can enable HTTPS directly within our Spring Boot application.
1.1. Goals
You will learn:
-
the basis of how HTTPS forms trusted, private communications
-
the difference between self-signed certificates and those signed by a trusted authority
-
how to enable HTTPS/TLS within our Spring Boot application
1.2. Objectives
At the conclusion of this lecture and related exercises, you will be able to:
-
define the purpose of a public certificate and private key
-
generate a self-signed certificate for demonstration use
-
enable HTTPS/TLS within Spring Boot
-
optionally implement an HTTP to HTTPS redirect
-
implement a Maven Failsafe integration test using HTTPS
2. HTTP Access
I have cloned the "noauth-security-example" to form the "https-hello-example" and left most of the insides intact. You may remember the ability to execute the following authenticated command.
$ curl -v -X GET http://localhost:8080/api/authn/hello?name=jim -u "user:password" (1) > GET /api/authn/hello?name=jim HTTP/1.1 > Host: localhost:8080 > Authorization: Basic dXNlcjpwYXNzd29yZA== (2) > < HTTP/1.1 200 hello, jim
1 | curl supports credentials with -u option |
2 | curl Base64 encodes credentials and adds Authorization header |
We get rejected when no valid credentials are supplied.
$ curl -X GET http://localhost:8080/api/authn/hello?name=jim {"timestamp":"2020-07-18T14:43:39.670+00:00","status":401, "error":"Unauthorized","message":"Unauthorized","path":"/api/authn/hello"}
It works as we remember it, but the issue is that our slightly encoded
(dXNlcjpwYXNzd29yZA==
), plaintext password was issued in the clear.
We can fix that by enabling HTTPS.
3. HTTPS
Hypertext Transfer Protocol Secure (HTTPS) is an extension of HTTP encrypted with Transport Layer Security (TLS) for secure communication between endpoints — offering privacy and integrity (i.e., hidden and unmodified). HTTPS formerly offered encryption with the now deprecated Secure Sockets Layer (SSL). Although the SSL name still sticks around, TLS is only supported today. [1] [2]
3.1. HTTPS/TLS
At the heart of HTTPS/TLS are X.509 certificates and the Public Key Infrastructure (PKI). Public keys are made available to describe the owner (subject), the issuer, and digital signatures that prove the contents have not been modified. If the receiver can verify the certificate and trusts the issuer — the communication can continue. [3]
With HTTPS/TLS, there is one-way and two-way option with one-way being the most common. In one-way TLS — only the server contains a certificate and the client is anonymous at the network level. Communications can continue if the client trusts the certificate presented by the server. In two-way TLS, the client also presents a signed certificate that can identify them to the server and form two-way authentication at the network level. Two-way is very secure but not as common except in closed environments (e.g., server-to-server environments with fixed/controlled communications). We will stick to one-way TLS in this lecture.
3.2. Keystores
A keystore is repository of security certificates - both private and public keys. There are two primary types: Public Key Cryptographic Standards (PKCS12) and Java KeyStore (JKS). PKCS12 is an industry standard and JKS is specific to Java. [4] They both have the ability to store multiple certificates and use an alias to identify them. Both use password protection.
There are typically two uses for keystores: your identity (keystore) and the identity of certificates you trust (truststore). The former is used by servers and must be well protected. The later is necessary for clients. The truststore can be shared but its contents need to be trusted.
3.3. Tools
There are two primary tools when working with certificates and keystores: keytool and openssl.
keytool comes with the JDK and can easily generate and manage certificates for Java applications. Keytool originally used the JKS format but since Java 9 switched over to PKCS12 format.
openssl is a standard, open source tool that is not specific to any environment. It is commonly used to generate and convert to/from all types of certificates/keys.
3.4. Self Signed Certificates
The words "trust" and "verify" were used a lot in the paragraphs above when describing certificates.
When we visit various web sites — that locked icon next to the "https" URL indicates the certificate presented by the server was verified and came from a trusted source. |
Verified Server Certificate
|
Trusted certificates come from sources that are pre-registered in the browsers and Java JRE truststore and are obtained through purchase.
We can generate self-signed certificates that are not immediately trusted until we either ignore checks or enter them into our local browsers and/or truststore(s).
4. Enable HTTPS/TLS in Spring Boot
To enable HTTPS/TLS in Spring Boot — we must do the following
-
obtain a digital certificate - we will generate a self-signed certificate without purchase or much fanfare
-
add TLS properties to the application
-
optionally add an HTTP to HTTPS redirect - useful in cases where clients forget to set the protocol to
https://
and usehttp://
or use the wrong port number.
4.1. Generate Self-signed Certificate
The following example shows the creation of a self-signed certificate using keytool. Refer to the keytool reference page for details on the options. The following Java Keytool page provides examples of several use cases. I kept the values of the certificate extremely basic since there is little chance we will ever use this in a trusted environment.
$ keytool -genkeypair -keyalg RSA -keysize 2048 -validity 3650 \(1)
-keystore keystore.p12 -alias https-hello \(2)
-storepass password
What is your first and last name?
[Unknown]: localhost
What is the name of your organizational unit?
[Unknown]:
What is the name of your organization?
[Unknown]:
What is the name of your City or Locality?
[Unknown]:
What is the name of your State or Province?
[Unknown]:
What is the two-letter country code for this unit?
[Unknown]:
Is CN=localhost, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown correct?
[no]: yes
1 | specifying a valid date 10 years in the future |
2 | assigning the alias https-hello to what is generated in the keystore |
4.2. Place Keystore in Reference-able Location
The keytool command output a keystore file called keystore.p12
. I placed that
in the resources area of the application — which will be can be referenced at runtime
using a classpath reference.
$ tree src/main/resources/
src/main/resources/
|-- application.properties
`-- keystore.p12
Incremental Learning Example Only: Don’t use Source Tree for Certs
This example is trying hard to be simple and using a classpath for the keystore to be portable.
You should already know how to convert the classpath reference to a file or other reference to keep sensitive information protected and away from the code base.
Do not store credentials or other sensitive information in the |
4.3. Add TLS properties
The following shows a minimal set of properties needed to enable TLS. [5]
server.port=8443(1)
server.ssl.enabled=true
server.ssl.key-store=classpath:keystore.p12(2)
server.ssl.key-store-password=password(3)
server.ssl.key-alias=https-hello
1 | using an alternate port - optional |
2 | referencing keystore in the classpath — could also use a file reference |
3 | think twice before placing credentials in a properties file |
Do not place credentials in CM system
Do not place real credentials in files checked into CM.
Have them resolved from a source provided at runtime.
|
Note the presence of the legacy "ssl" term in the property name even though "ssl" is deprecated and we are technically setting up "tls". |
5. Untrusted Certificate Error
Once we restart the server, we should be able to connect using HTTPS and port 8443. However, there will be a trust error. The following shows the error from curl.
$ curl https://localhost:8443/api/authn/hello?name=jim -u user:password
curl: (60) SSL certificate problem: self signed certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
6. Accept Self-signed Certificates
curl and older browsers have the ability to accept self-signed certificates either by ignoring their inconsistencies or adding them to their truststore.
The following is an example of curl’s -insecure
option (-k
abbreviation)
that will allow us to communicate with a server presenting a certificate that
fails validation.
$ curl -kv -X GET https://localhost:8443/api/authn/hello?name=jim -u "user:password"
* Connected to localhost (::1) port 8443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
* CAfile: /etc/ssl/cert.pem
CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server did not agree to a protocol
* Server certificate:
* subject: C=Unknown; ST=Unknown; L=Unknown; O=Unknown; OU=Unknown; CN=localhost
* start date: Jul 18 13:46:35 2020 GMT
* expire date: Jul 16 13:46:35 2030 GMT
* issuer: C=Unknown; ST=Unknown; L=Unknown; O=Unknown; OU=Unknown; CN=localhost
* SSL certificate verify result: self signed certificate (18), continuing anyway.
* Server auth using Basic with user 'user'
> GET /api/authn/hello?name=jim HTTP/1.1
> Host: localhost:8443
> Authorization: Basic dXNlcjpwYXNzd29yZA==
>
< HTTP/1.1 200
hello, jim
6.1. Optional Redirect
To handle clients that may address our application using the wrong protocol or port number — we can optionally setup a redirect to go from the common port to the TLS port. The following snippet was taken directly from a ZetCode article but I have seen this near exact snippet many times elsewhere.
@Bean
public ServletWebServerFactory servletContainer() {
var tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(redirectConnector());
return tomcat;
}
private Connector redirectConnector() {
var connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8080);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
6.2. HTTP:8080 ⇒ HTTPS:8443 Redirect Example
With the optional redirect in place, the following shows an example of the client being
sent from their original http://localhost:8080
call to https://localhost:8443
.
$ curl -kv -X GET http://localhost:8080/api/authn/hello?name=jim -u "user:password" > GET /api/authn/hello?name=jim HTTP/1.1 > Host: localhost:8080 > Authorization: Basic dXNlcjpwYXNzd29yZA== > < HTTP/1.1 302 (1) < Location: https://localhost:8443/api/authn/hello?name=jim (2)
1 | HTTP 302/Redirect Returned |
2 | Location header provides the full URL to invoke — including the protocol |
6.3. Follow Redirects
Browsers automatically follow redirects and we can get curl to automatically follow
redirects by adding the --location
option (or -L
abbreviated). The following
command snippet shows curl being requested to connect to an HTTP port , receiving
a 302/Redirect, and then completing the original command using the URL provided
in the Location
header of the redirect.
$ curl -kvL -X GET http://localhost:8080/api/authn/hello?name=jim -u "user:password" (1)
> GET /api/authn/hello?name=jim HTTP/1.1
> Host: localhost:8080
> Authorization: Basic dXNlcjpwYXNzd29yZA==
>
< HTTP/1.1 302
< Location: https://localhost:8443/api/authn/hello?name=jim
<
* Issue another request to this URL: 'https://localhost:8443/api/authn/hello?name=jim'
...
* Server certificate:
* subject: C=Unknown; ST=Unknown; L=Unknown; O=Unknown; OU=Unknown; CN=localhost
* start date: Jul 18 13:46:35 2020 GMT
* expire date: Jul 16 13:46:35 2030 GMT
* issuer: C=Unknown; ST=Unknown; L=Unknown; O=Unknown; OU=Unknown; CN=localhost
* SSL certificate verify result: self signed certificate (18), continuing anyway.
> GET /api/authn/hello?name=jim HTTP/1.1
> Host: localhost:8443
> Authorization: Basic dXNlcjpwYXNzd29yZA==
>
< HTTP/1.1 200
hello, jim
1 | -L (--location) redirect option causes curl to follow the 302/Redirect response |
6.4. Caution About Redirects
One note of caution I will give about redirects is the tendency for Intellij to leave orphan processes which seems to get worse with the Tomcat redirect in place. Since our targeted interfaces are for API clients — which should have a documented source of how to communicate with our server — there should be no need for the redirect. The redirect is primarily valuable for interfaces that switch between HTTP and HTTPS we are either all HTTP or all HTTPS and no need to be eclectic.
Eliminating the optional redirect also eliminates the need for the redirect code and reduces our required steps to obtaining the certificate and setting a few simple properties.
7. Maven Integration Test
Since we are getting close to real deployments to test environments and we have hit unit integration tests pretty hard, I wanted to demonstrate a test of the HTTPS configuration using a true integration test and the Maven Failsafe plugin.
Figure 1. Maven Failsafe Integration Test
|
A Maven Failsafe integration test is very similar to the other Web API unit integration tests you are use to seeing in this course. The primary difference is that there are no server-side components in the JUnit Spring context. All the server-side components are in a separate executable. The following diagram shows the participants that directly help to implement the integration test. This will be accomplished with the aid of the Maven Failsafe, Spring Boot, and Build Maven Helper plugins. |
With that said, we will still want to be able to execute simple integration tests like this within the IDE. Therefore expect some setup aspects to support both IDE-based and Maven-based integration testing setup in the paragraphs that follow.
7.1. Maven Integration Test Phases
Maven executes integration tests using four (4) phases
-
pre-integration-test - start resources
-
integration-test - execute tests
-
post-integration-test - stop resources
-
verify - evaluate/assert test results
We will make use of three (3) plugins to perform that work within Maven. Each is also accompanied by steps to mimic the Maven capability on a small scale with the IDE:
-
spring-boot-maven-plugin
- used to start and stop the server-side Spring Boot process-
(use IDE, "java -jar" command, or "mvn springboot:run" command to manually start, restart, and stop the server)
-
-
build-maven-helper-plugin
- used to allocate a random network port for server-
(within the IDE you will use a property file that uses a well-known port# used one-at-a-time)
-
-
maven-failsafe-plugin
- used to run the JUnit JVM with the tests — passing in the port# — and verifying/asserting the results.-
(use IDE to run test following server-startup)
-
7.2. Spring Boot Maven Plugin
The spring-boot-maven-plugin
will be configured with at least 2 executions to support Maven integration testing.
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
...
</executions>
</plugin>
7.2.1. SpringBoot: pre-integration-test Phase (start)
The following snippet shows the plugin being used to start
the server in the background (versus a blocking run
).
The execution is configured to supply a Spring Boot server.port
property with the HTTP port to use.
We will use a separate plugin to generate the port number and have that assigned to the Maven server.http.port
property at build time.
The client-side Spring Boot Test will also need this port value for the client(s) in the integration tests.
<execution>
<id>pre-integration-test</id> (1)
<phase>pre-integration-test</phase> (2)
<goals>
<goal>start</goal> (3)
</goals>
<configuration>
<skip>${skipITs}</skip> (4)
<arguments> (5)
<argument>--server.port=${server.http.port}</argument>
</arguments>
</configuration>
</execution>
1 | each execution must have a unique ID |
2 | this execution will be tied to the pre-integration-test phase |
3 | this execution will start the server in the background |
4 | -DskipITs=true will deactivate this execution |
5 | --server.port is being assigned at runtime and used by server for HTTP/S listen port |
Failsafe is overriding the fixed value from application-https.properties
.
server.port=8443
The above execution phase has the same impact as if we launched the JAR manually with spring.profiles.active
and whether server.port
was supplied on the command.
This allows multiple IT tests to run concurrently without colliding on network port number.
It also permits the use of a well-known/fixed value for use with IDE-based testing.
$ java -jar target/https-hello-example-*-SNAPSHOT.jar --spring.profiles.active=https
Tomcat started on port(s): 8443 (https) with context path ''(1)
$ java -jar target/https-hello-example-*-SNAPSHOT.jar --spring.profiles.active=https --server.port=7712 (2)
Tomcat started on port(s): 7712 (http) with context path '' (2)
1 | Spring Boot using well-known/fixed port# supplied in application-https.properties |
2 | Spring Boot using runtime server.port property to override port to use |
7.2.2. SpringBoot: post-integration-test Phase (stop)
The following snippet shows the Spring Boot plugin being used to stop
a running server.
<execution>
<id>post-integration-test</id> (1)
<phase>post-integration-test</phase> (2)
<goals>
<goal>stop</goal> (3)
</goals>
<configuration>
<skip>${skipITs}</skip> (4)
</configuration>
</execution>
1 | each execution must have a unique ID |
2 | this execution will be tied to the post-integration-test phase |
3 | this execution will stop the running server |
4 | -DskipITs=true will deactivate this execution |
skipITs support
Most plugins offer a skip option to bypass a configured execution and sometimes map that to a Maven property that can be expressed on the command line.
Failsafe maps their property to skipITs .
By mapping the Maven skipITs property to the plugin’s skip configuration element, we can inform related plugins to do nothing.
This allows one to run the Maven install phase without requiring integration tests to run and pass.
|
7.3. Build Helper Maven Plugin
The build-helper-maven-plugin
contains various utilities that are helpful to create a repeatable, portable build.
We are using the reserve-network-port
goal to select an available HTTP port at build-time.
The allocated port number is assigned to the Maven server.http.port
property.
This was shown picked up by the Spring Boot Maven Plugin earlier.
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>reserve-network-port</id>
<phase>process-resources</phase> (1)
<goals>
<goal>reserve-network-port</goal> (2)
</goals>
<configuration>
<portNames> (3)
<portName>server.http.port</portName>
</portNames>
</configuration>
</execution>
</executions>
</plugin>
1 | execute during the process-resources Maven phase — which is well before pre-integration-test |
2 | execute the reserve-network-port goal of the plugin |
3 | assigned the identified port to the Maven server.http.port property |
7.4. Failsafe Plugin
The Failsafe plugin has some default behavior, but once we start configuring it — we need to restate much of what it would have done automatically for us.
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
...
</executions>
</plugin>
7.4.1. Failsafe: integration-test Phase
In the snippet below, we are primarily configuring Failsafe to launch the JUnit test with an it.server.port
property.
This will be read in by the ServerConfig
@ConfigurationProperties
class
<execution>
<id>integration-test</id>
<phase>integration-test</phase> (1)
<goals> (1)
<goal>integration-test</goal>
</goals>
<configuration>
<includes> (1)
<include>**/*IT.java</include>
</includes>
<systemPropertyVariables> (2)
<it.server.port>${server.http.port}</it.server.port>
</systemPropertyVariables>
<additionalClasspathElements> (3)
<additionalClasspathElement>${basedir}/target/classes</additionalClasspathElement>
</additionalClasspathElements>
<useModulePath>false</useModulePath> (4)
</configuration>
</execution>
1 | re-state some Failsafe default relative to phase, goal, includes |
2 | add a -Dit.server.port=${server.http.port} system property to the execution |
3 | adding target/classes to classpath when JUnit test using classes from "src/main" |
4 | turning off some Java 9 module features |
Full disclosure.
I need to refresh my memory on exactly why default additionalClasspathElements and useModulePath did not work here.
|
7.4.2. Failsafe: verify Phase
The snippet below shows the final phase for Failsafe. After the integration resources have been taken down, the only thing left is to assert the results. This pass/fail assertion is delayed a few phases so that the build does not fail while integration resources are still running.
<execution>
<id>verify</id>
<phase>verify</phase>
<goals>
<goal>verify</goal>
</goals>
</execution>
7.5. JUnit @SpringBootTest
With the Maven setup complete — that brings us back to a familiar looking JUnit test and @SpringBootTest
However, there is no application or server-side resources in the Spring context,
@SpringBootTest(classes={ClientTestConfiguration.class}, (1)
webEnvironment = SpringBootTest.WebEnvironment.NONE) (2)
@ActiveProfiles({"its"}) (3)
public class HttpsRestTemplateIT {
@Autowired (4)
private RestTemplate authnUser;
@Autowired (5)
private URI authnUrl;
1 | no application class in this integration test. Everything is server-side. |
2 | have only a client-side web environment. No listen port necessary |
3 | activate its profile for scope of test case |
4 | inject RestTemplate configured with user credentials that can authenticate |
5 | inject URL to endpoint test will be calling |
Since we have no RANDOM_PORT and a late @LocalServerPort injection, we can move ServerConfig to the configuration class and inject the baseURL product.
|
7.6. ClientTestConfiguration
This trimmed down @Configuration
class is all that is needed for JUnit test to be a client of a remote process.
The @SpringBootTest
will demand to have a @SpringBootConfiguration
and we technically do not have the @SpringBootApplication
during the test.
@SpringBootConfiguration(proxyBeanMethods = false)
@EnableAutoConfiguration
@Slf4j
public class ClientTestConfiguration {
...
@Bean
@ConfigurationProperties("it.server")
public ServerConfig itServerConfig() { ...
@Bean
public URI authnUrl(ServerConfig serverConfig) { ...(1)
@Bean
public RestTemplate authnUser(RestTemplateBuilder builder,...(2)
...
1 | baseUrl of remote server |
2 | RestTemplate with authentication and HTTPS filters applied |
7.7. application-its.properties
The following snippet shows the its
profile-specific configuration file, complete with
-
it.server.scheme
(https) -
it.server.port
(8443) -
trustStore properties pointing at the server-side identity keystore.
it.server.scheme=https
#must match self-signed values in application-https.properties
it.server.trust-store=keystore.p12
it.server.trust-store-password=password
#used in IDE, overridden from command line during failsafe tests
it.server.port=8443 (1)
1 | default port when working in IDE. Overridden by command line properties by Failsafe |
The keystore/truststore used in this example is for learning and testing. Do not store operational certs in the source tree. Those files end up in the searchable CM system and the JARs with the certs end up in a Nexus repository. |
7.8. username/password Credentials
The following shows the username and password credentials being injected using values from the properties. In this test’s case — they should always be provided. Therefore, no default String is defined.
public class ClientTestConfiguration {
@Value("${spring.security.user.name}")
private String username;
@Value("${spring.security.user.password}")
private String password;
7.9. ServerConfig
The following shows the primary purpose for ServerConfig
as a @ConfigurationProperties
class with flexible prefix.
In this particular case it is being instructed to read in all properties with prefix "it.server" and instantiate a ServerConfig.
@Bean
@ConfigurationProperties("it.server")
public ServerConfig itServerConfig() {
return new ServerConfig();
}
From the property file earlier, you will notice that the URL scheme will be "https" and the port will be "8443" or whatever property override is supplied on the command line.
The resulting value will be injected into the @Configuration
class.
7.10. authnUrl URI
Since we don’t have the late-injected @LocalServerPort
for the web-server and our ServerConfig
is now all property-based, we can now delegate baseUrls to injectable beans.
The following shows the baseUrl
from ServerConfig
being used to construct a URL for "api/authn/hello".
@Bean
public URI authnUrl(ServerConfig serverConfig) {
URI baseUrl = serverConfig.getBaseUrl();
return UriComponentsBuilder.fromUri(baseUrl).path("/api/authn/hello").build().toUri();
}
7.11. authUser RestTemplate
By no surprise, authnUser()
is adding a BasicAuthenticationInterceptor
containing the injected username and password to a new RestTemplate
for use in the test.
The injected ClientHttpRequestFactory
will take care of the HTTP/HTTPS details.
@Bean
public RestTemplate authnUser(RestTemplateBuilder builder,
ClientHttpRequestFactory requestFactory) {
RestTemplate restTemplate = builder.requestFactory(
//used to read the streams twice -- so we can use the logging filter below
()->new BufferingClientHttpRequestFactory(requestFactory))
.interceptors(new BasicAuthenticationInterceptor(username, password),
new RestTemplateLoggingFilter())
.build();
return restTemplate;
}
7.12. HTTPS ClientHttpRequestFactory
The HTTPS-based ClientHttpRequestFactory is built by following some excellent instructions and short article provided by Geoff Bourne. The following intermediate factory relies on the ability to construct an SSLContext.
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.client.ClientHttpRequestFactory;
/*
TLS configuration based on great/short article by Geoff Bourne
https://medium.com/@itzgeoff/using-a-custom-trust-store-with-resttemplate-in-spring-boot-77b18f6a5c39
*/
@Bean
public ClientHttpRequestFactory httpsRequestFactory(SSLContext sslContext,
ServerConfig serverConfig) {
HttpClient httpsClient = HttpClientBuilder.create()
.setSSLContext(serverConfig.isHttps() ? sslContext : null)
.build();
return new HttpComponentsClientHttpRequestFactory(httpsClient);
}
7.13. SSL Context
The SSLContext @Bean
factory locates and loads the trustStore based on the properties within ServerConfig
.
If found, it uses the SSLContextBuilder
from the apache HTTP libraries to create a SSLContext
.
import org.apache.http.ssl.SSLContextBuilder;
import javax.net.ssl.SSLContext;
...
@Bean
public SSLContext sslContext(ServerConfig serverConfig) {
try {
URL trustStoreUrl = null;
if (serverConfig.getTrustStore()!=null) {
trustStoreUrl = HttpsExampleApp.class.getResource("/" + serverConfig.getTrustStore());
if (null==trustStoreUrl) {
throw new IllegalStateException("unable to locate truststore:/" + serverConfig.getTrustStore());
}
}
SSLContextBuilder builder = SSLContextBuilder.create()
.setProtocol("TLSv1.2");
if (trustStoreUrl!=null) {
builder.loadTrustMaterial(trustStoreUrl, serverConfig.getTrustStorePassword());
}
return builder.build();
} catch (Exception ex) {
throw new IllegalStateException("unable to establish SSL context", ex);
}
}
7.14. JUnit @Test
The core parts of the JUnit test are pretty basic once we have the HTTPS/Authn-enabled RestTemplate
and baseUrl injected.
From here it is just a normal test, but activity is remote on the server side.
public class HttpsRestTemplateIT {
@Autowired (1)
private RestTemplate authnUser;
@Autowired (2)
private URI authnUrl;
@Test
public void user_can_call_authenticated() {
//given a URL to an endpoint that accepts only authenticated calls
URI url = UriComponentsBuilder.fromUri(authnUrl)
.queryParam("name", "jim").build().toUri();
//when called with an authenticated identity
ResponseEntity<String> response = authnUser.getForEntity(url, String.class);
//then expected results returned
then(response.getStatusCode()).isEqualTo(HttpStatus.OK);
then(response.getBody()).isEqualTo("hello, jim");
}
}
1 | RestTemplate with authentication and HTTPS aspects addressed using filters |
2 | authnUrl built from ServerConfig and injected into test |
7.15. Maven Verify
When we execute mvn verify
(with option to add clean
), we see the port being determined and assigned to the server.http.port
Maven property.
$ mvn verify ... - build-helper-maven-plugin:3.1.0:reserve-network-port (reserve-network-port) Reserved port 52024 for server.http.port (1) ...(2) - maven-surefire-plugin:3.0.0-M5:test (default-test) @ https-hello-example --- ... - spring-boot-maven-plugin:2.4.2:repackage (package) @ https-hello-example --- Replacing main artifact with repackaged archive (3) - spring-boot-maven-plugin:2.4.2:start (pre-integration-test) @ https-hello-example ---
1 | the port identified by build-helper-maven-plugin as 52024 |
2 | Surefire tests firing at an earlier test phase |
3 | server starting in the pre-integration-test phase |
7.15.1. Server Output
When the server starts, we can see that the https
profile is activate and Tomcat was assigned the 52024
port value from the build.
HttpsExampleApp#logStartupProfileInfo:664 The following profiles are active: https (1) TomcatWebServer#initialize:108 Tomcat initialized with port(s): 52024 (https) (2) TomcatWebServer#start:220 Tomcat started on port(s): 52024 (https) with context path '' (2)
1 | https profile has been activated on the server |
2 | server HTTP(S) port assigned to 52024 |
7.15.2. JUnit Client Output
When the JUnit client starts, we can see that SSL is enabled and the baseURL contains https
and the dynamically assigned port 52024
.
HttpsRestTemplateIT#logStartupProfileInfo:664 The following profiles are active: its (1) ClientTestConfiguration#authnUrl:64 baseUrl=https://localhost:52024 (2) ClientTestConfiguration#authnUser:107 enabling SSL requests (3)
1 | its profile is active in JUnit client |
2 | baseUrl is assigned https and port 52024 , with the latter dynamically assigned at build-time |
3 | SSL has been enabled on client |
7.15.3. JUnit Test DEBUG
There is some DEBUG logged during the activity of the test(s).
GET /api/authn/hello?name=jim, headers=[accept:"text/plain, application/json, application/xml, application/*+json, text/xml, application/*+xml, */*", authorization:"Basic dXNlcjpwYXNzd29yZA==", host:"localhost:52024", connection:"Keep-Alive", user-agent:"masked", accept-encoding:"gzip,deflate"]]
7.15.4. Failsafe Test Results
Test results are reported.
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 2.409 s - in info.ejava.examples.svc.https.HttpsRestTemplateIT [INFO] Results: [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
7.15.5. Server is Stopped
Server is stopped.
[INFO] --- spring-boot-maven-plugin:2.4.2:stop (post-integration-test) [INFO] Stopping application... 15:29:42.178 RMI TCP Connection(4)-127.0.0.1 INFO XBeanRegistrar$SpringApplicationAdmin#shutdown:159 Application shutdown requested.
7.15.6. Test Results Asserted
Test results are asserted.
[INFO] [INFO] --- maven-failsafe-plugin:3.0.0-M5:verify (verify) @ https-hello-example --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS
8. Summary
In this module we learned:
-
the basis of how HTTPS forms trusted, private communications
-
how to generate a self-signed certificate for demonstration use
-
how to enable HTTPS/TLS within our Spring Boot application
-
how to add an optional redirect and why it may not be necessary
-
how to setup and run a Maven Failsafe Integration Test