If you’ve ever deployed Spring Boot behind a load balancer, you might be aware of issues coming from differences between the request into the load balancer and the request into your application. These requests will often have a different protocol, host, or port. If Spring Boot isn’t correctly setup it can lead to all sorts of mayhem, such as generating incorrect URLs for you application.
In this article you’ll discover how to make use of the X-Forwarded
headers passed from a load balancer to your Spring Boot application, to help your application generate URLs correctly based on the originating request.
Spring Boot applications behind a load balancer
A common setup for a production Spring Boot application is to deploy it behind a load balancer. This has the benefit of providing:
- scalability since if you add more instances of your application then load will be spread between them
- security because a) the load balancer can handle HTTPS certificates and b) it provides a single public entry point into your application
Notice that in the above diagram the traffic is:
- encrypted over HTTPS between the client and the load balancer
- unencrypted over HTTP between the load balancer and the application
This setup is good enough for many organisations, since even though the internal traffic is unencrypted, the private nature of this network means there is minimal risk of it being intercepted by a bad actor.
So all good then? Not exactly, it turns out that in the above setup Spring Boot finds itself with a bit of an identify crisis.
Potential issues behind a load balancer
Imagine your Spring Boot application for whatever reason needs to return an absolute URL. Remember that an absolute URL is one that includes the domain, such as https://archive.tomgregory.com/category/general-coding
.
Reasons for this include:
- when an API response is returned with HATEOAS links to other APIs (Spring Boot’s
/actuator
API is a great example of this) - making links always use absolute URLs to avoid duplicate content issues for search engine crawlers
Generating absolute URLs in Spring Boot
A common way to implement the generation of absolute URLs is to use the information in the request to generate the URL, including the protocol, host, port, and path. Spring Boot provides the following mechanism to do this:
ServletUriComponentsBuilder.fromCurrentRequest_().build().toUriString();
This code will generate the full URL of the current request including path. Behind the scenes it uses the HttpServletRequest
methods getScheme()
, getServerName()
, and getServerPort()
to provide the required URL components.
Try it out
The accompanying GitHub repository contains a Spring Boot project with several endpoints we’ll use throughout this article.
Run the application with ./gradlew bootRun
then run curl http://localhost:8080/absoluteURL
to get the application to generate an absolute URL as described above. You’ll see this:
$ curl http://localhost:8080/absoluteURL
http://localhost:8080/absoluteURL
The issue with URL generation
If the request from the client is getting passed directly to the Spring Boot application, then the above approach is fine. If there is a load balancer in between though, the generated URL will be representative of the request that was made from the load balancer to the application, and not the request made into the load balancer itself.
To illustrate this, here’s a response that was generated from a Spring Boot application’s /actuator
endpoint. The application sits behind a load balancer, and the request was made to https://forwarded-header-filter-example.tomgregory.com/actuator
.
{
"self": {
"href": "http://forwarded-header-filter-example.tomgregory.com/actuator",
"templated": false
},
"health": {
"href": "http://forwarded-header-filter-example.tomgregory.com/actuator/health",
"templated": false
},
"health-path": {
"href": "http://forwarded-header-filter-example.tomgregory.com/actuator/health/{*path}",
"templated": true
}
}
See any problem? That’s right, the generated URLs have the http
rather than https
protocol. Anyone following these links is going to get a big dose of disappointment because they won’t work. 😈
Wouldn’t it be nice if there were a way we could use the details of the originating request to generate an absolute URL?
Introducing the X-Forwarded headers
Fortunately, a solution for this problem already exists with X-Forwarded
headers. These special headers are often set by load balancers to tell downstream services where the original request came from. They include:
- x-forwarded-proto the originating request’s protocol (HTTP/HTTPS)
- x-forwarded-port the originating request’s port
- x-forwarded-for the originating IP address
There are other X-Forwarded
headers but we only need the first two to achieve our desired outcome. These headers are added by default to requests passing through the AWS Application Load Balancer, and other cloud providers most likely support them.
Here is a full list of headers that were passed through to the example Spring Boot application, after I deployed it into AWS behind an application load balancer. The request was made running curl https://forwarded-header-filter-example.tomgregory.com/absoluteURL
.
"headers": {
"x-amzn-trace-id": [
"Root=1-5f800790-3994edf14fa982914c388707"
],
"x-forwarded-proto": [
"https"
],
"host": [
"forwarded-header-filter-example.tomgregory.com"
],
"x-forwarded-port": [
"443"
],
"x-forwarded-for": [
"73.59.102.103"
],
"user-agent": [
"curl/7.68.0"
],
"accept": [
"*/*"
]
}
Even though the request into the Spring Boot application from the load balancer was over HTTP, you can clearly see above that we have information about the originating request, including:
- the originating protocol of
https
- the originating port of
443
- the originating IP address of
73.59.102.103
Using X-Forwarded headers with Spring Boot
Fortunately, the clever people over at Spring HQ have a solution for this in the form of the request filter class ForwardedHeaderFilter
. Remember that in Spring a request filter is a class that does some processing before the request is passed to the controller.
So what exactly does this filter do? Well, here’s what the docs say:
Extract values from “Forwarded” and “X-Forwarded-*” headers, wrap the request and response, and make they reflect the client-originated protocol and address in the following methods:
getServerName() getServerPort() getScheme() isSecure() sendRedirect(String)
That’s very cool, because those are exactly the methods used to generate the absolute URL in the example application. So let’s try this out then!
Configuring the ForwardedHeaderFilter
To add the ForwardedHeaderFilter
into your application you need to configure a FilterRegistrationBean
bean like this:
@Bean
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter);
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
registration.setUrlPatterns(List.of("/absoluteURLWithFilter"));
return registration;
}
- we create the filter and wrap it in a filter registration bean
- we set the dispatcher types according to this Spring documentation
- we set the highest precedence ordering so that this filter gets applied first, before any others. This is important in case any other filters need to use the wrapped HTTP request.
- we configure the filter to apply to only a specific URL using a URL pattern
Demo of Spring Boot behind a load balancer with X-Forwarded headers
Let’s cut to the chase and see what happens when a Spring Boot application deployed behind a load balancer tries to generate a URL with and without the ForwardedHeaderFilter
. This has been tested in AWS using the below setup:
- the Spring Boot application is deployed behind an AWS Application Load Balancer
- access to the load balancer is over HTTPS
- access from the load balancer to the application is over HTTP
- there are two endpoints exposed on the application which generate an absolute URL in the same way using the
ServletUriComponentsBuilder
, as described earlier/absoluteURL
generates the URL with no filters applied/absoluteURLWithFilter
generates the URL but with theForwardedHeaderFilter
applied first
So, here are the results of this little experiment…
No filter
When I run curl https://forwarded-header-filter-example.tomgregory.com/absoluteURL
I get a response of http://forwarded-header-filter-example.tomgregory.com/absoluteURL
. Wrong protocol. 😢
With filter
But when I run curl https://forwarded-header-filter-example.tomgregory.com/absoluteURLWithFilter
I get a response of https://forwarded-header-filter-example.tomgregory.com/absoluteURLWithFilter
. Much better!
You can see that when the ForwardedHeaderFilter
is applied we get the HTTPS protocol in the generated URL, which comes from the X-Forwarded-Proto
header added by the load balancer. Awesome! ✅
Try it out yourself in AWS
Sadly I can’t leave this service running all the time for you to try out. Ain’t nobody got CPU for that! Instead, why not launch this CloudFormation stack into your own AWS account using the button below?
This will launch all the resources required to get this example up and running, including a VPC, subnets, load balancer, and Spring Boot application deployment using AWS Elastic Container Service (see template for full details).
You’ll need to set the CertificateArn parameter to apply this template. This is the ARN (Amazon Resource Name) of a certificate which will be used to encrypt traffic into the load balancer. You can create one by going to Services > Certificate Manager > Request a certificate.
Once the stack has launched go to Services > EC2 > Load Balancers then select the load balaner and copy the DNS name. You can then access the application on:
https://<load-balancer-dns-name>:443/absoluteURL
https://<load-balancer-dns-name>:443/absoluteURLWithFilter
You’ll have to accept the invalid certificate warnings, as the certificate doesn’t match the load balancer DNS name. If you’re using curl
pass the --insecure
flag.
To resolve this problem properly, the certificate used by this CloudFormation template must be created for your own domain, then a CNAME
record pointing to the load balancer should be added in your domain provider’s DNS settings.
Don’t forgot to delete the CloudFormation stack once you’re finished playing to avoid incurring any unnecessary charges.
Summary
If you’re deploying a Spring Boot application behind a load balancer, then consider using Spring’s ForwardedHeaderFilter
. This filter will help by making sure X-Forwarded
headers passed through from the load balancer are considered, preventing problems with generated URLs having the wrong protocol or port.
Resources
- check out the example Spring Boot project in GitHub
- Spring has some useful documentation on the ForwardedHeaderFilter
- launch the CloudFormation stack to see the example from this article working behind a load balancer, by clicking the magic button below