Forward remote IP address from Nginx to a Spring Boot application

Published by Alexander Braun on 30 Oct 2017 - tagged with Nginx, SSL, Security

This post explains how to forward the remote IP address of a client to a Spring Boot application when routing traffic through Nginx (reverse proxy).

Introduction

Currently, my preferred way to set up a Spring Boot based web application in production is to use a Reverse Proxy to route traffic to the corresponding backed applications. In this context, I really like Nginx as a fast and reliable web server with low memory footprint. The problem though, when using a reverse proxy is: when requests are forwarded to the backend applications, the remote IP address of the client is being replaced by the IP address of the reverse proxy server (e.g. 127.0.0.1 when installed on the same server).

To log requests and analyze user and behavioral patterns, it is beneficial to be able to use the remote IP address. This post explains how this can be achieved easily using Nginx.

1. Nginx configuration

The following fragment shows a basic section of a typical Nginx configuration file.

server {
    listen       80 default_server;
    server_name  example.com;

    location / {
        proxy_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://localhost:8080;
    }
}

Let's take a closer look at the location section. We use this section to add the values we need downstream - in our backend application - as request headers.

X-Forwarded-Host

Specifies the hostname that was requested by the client. This typically includes the requested port.

X-Forwarded-Server

Specifies the hostname of the reverse proxy server.

X-Forwarded-For

Specifies the IP address of the client requesting the page. The variable $proxy_add_x_forwarded_for stores the remote IP address and we simply copy the actual value to the X-Forwarded-For header.

List of available variables can be found here.

2. Extract the header in the Spring Boot web application

In case you want to log the remote IP address for all requests to the backend, I recommend to the create a HandlerIntercepter that will automatically extract the information from all requests and executes the logic to store the data.

HandlerInterceptor implementation

The class below shows a simple HandlerInterceptor that extracts and logs the remote IP address in conjunction with the requested URI. As we specified above that Nginx should provide the remote IP address in the header named "X-Forwarded-For", we can simply extract the value with a statement like request.getHeader("X-Forwarded-For").

The full example of our HandlerInterceptor implementation is below:

package com.progressive.code.starter.interceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

/* Name "RequestLogger" to be able to use @Autowired in the configuration */
@Component("RequestLogger")
public class RequestLogger implements HandlerInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(
        RequestLogger.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
        Object handler)
            throws Exception {

        /* Logs the "wrong" ip - the ip of the reverse proxy, in our example 127.0.0.1 */
        LOGGER.info("Page {} was requested by IP {}", request.getRequestURI(),
            request.getRemoteAddr());
        /* Logs the "real" remote ip address, as set by Nginx */
        LOGGER.info("Page {} was requested by IP {}", request.getRequestURI(),
            request.getHeader("X-Forwarded-For"));

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                Object handler,ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
                Object handler, Exception ex) throws Exception {
    }
}

Configure the Interceptor

The last step is to configure the Spring Boot Web MVC component to use our new RequestLogger. I prefer to name each HandlerInterceptor I create and use @Autowired in conjunction with @Qualifier to inject the interceptors to use. After the interceptor has been injected we only need to register it using a statement like registry.addInterceptor(this.requestLogger);

package com.progressive.code.starter.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    @Autowired
    @Qualifier("RequestLogger")
    HandlerInterceptor requestLogger;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.requestLogger);
    }
}

3. Test the implementation

When sending a request the application now creates the following example log entries:

Page / was requested by IP 127.0.0.1
Page / was requested by IP 52.128.128.10

As mentioned above the first log entry represents the reverse proxy IP and the second one the remote IP.