Objective: get the remote address of users (i.e. request.getRemoteAddr()
) in Spring Boot web applications.
Introduction:
As known, the getRemoteAddr
method returns the address of the immediate caller which might be the proxy server or any other servers in between the original user and the final destination server (if there's any). To find the real address of the user, one needs to look into other headers (e.g. X-Forwarded-For
). However I found that Tomcat (which is our web container) has a filter called RemoteIpFilter
that takes care of this issue by copying the original address, stored in another header (e.g. X-Forwarded-For
) back so that calling request.getRemoteAddr()
gives you the originl address.
I added the filter to the code and everything works just fine:
@Bean
FilterRegistrationBean<RemoteIpFilter> remoteIpFilter() {
val filter = new FilterRegistrationBean<>(new RemoteIpFilter());
filter.addUrlPatterns("/*");
return filter;
}
However, there's an issue; when the request is a multipart one, Spring rejects it with:
The request was rejected because the header value "multipart/mixed; boundary="----=_Part_0_blahblah"; charset=utf-8" is not allowed.
I can see that the exception is thrown because FilterChainProxy
filter of Spring wraps the request in StrictFirewalledRequest
that in turn, has some checks regarding what is allowed and what is not (e.g. the above content-type is not allowed).
I couldn't find a proper solution for the above issue, other than putting the RemoteIpFilter
filter before FilterChainProxy
filter:
// right before the tracing filter!
filter.setOrder(SleuthWebProperties.TRACING_FILTER_ORDER - 1);
Questions
- Is using
RemoteIpFilter
a good idea? - Is there a better way to fix the above issue?
- What's the best order number for
RemoteIpFilter
?
Thanks.
CodePudding user response:
Thanks to Piotr P. Karwasz's comment I figured out how to do this. It was not straightforward since it depends on the environment and other components (e.g. LB, reverse proxy, etc). But this is how you would go about it:
- Don't use the
RemoteIpFilter
if you're using recent versions of Spring Boot Tomcat. Spring Boot's got you covered by creating aRemoteIpValue
(inTomcatWebServerFactoryCustomizer
), if it feels the need. - If you're deploying on some cloud platform, chances are you don't need to do anything, because if Spring detects the platform as a cloud one, it automatically creates the value. No need to tell it explicitly. (See what
CloudPlatform.getActive(env)
returns.) - If you're not deploying on a cloud platform, probably you need to set at least one of the following properties:
ServerProperties#forwardHeadersStrategy = NATIVE
(as Piotr suggested)ServerProperties.Tomcat.Remoteip#protocolHeader
(to anything e.g.X-Forwarded-Proto
)ServerProperties.Tomcat.Remoteip#remoteIpHeader
(to anything e.g.X-Forwarded-For
)
- To see if it really worked, enabled debug log for
org.apache.catalina.valves.RemoteIpValve
and you should be able to see its logs about changing host, addresses, etc.
In our case, I didn't need to do anything on Spring, since we deploy our app to K8S on which Spring creates the valve. Then why didn't I get the correct remote address? It turns out that our reverse proxy, Nginx has the default configuration:
proxy_set_header X-Forwarded-For $remote_addr;
that essentially overwrites the original value, that is to be used by the RemoteIpValve
to set back the remote address. In other words, the valve correctly reads X-Forwarded-For
header but the header doesn't contain the correct value (the address of the user). Note that $remote_addr
is not necessarily the address of the user. It might be the address of some other intermediary proxy/LB.
So you've got two options to fix this:
- Set
ServerProperties.Tomcat.Remoteip#remoteIpHeader
toX-Original-Forwarded-For
because that's where Nginx backs up the original value ofX-Forwarded-For
before overwriting with$remote_addr
. (Seems like a hack to me!) - Configure Nginx to not touch
X-Forwarded-For
header at all. (Seems like a hack to me!) - Configure Nginx to append
X-Forwarded-For
instead of overwriting it.
To configure Nginx to append, you've got two options:
- Work on something like this
proxy_set_header X-Forwarded-For "$remote_addr, $server_addr"
and configure Nginx to append instead of overwrite. (See this for how to do it on K8S. Though I couldn't get it working; Nginx complained about the number of arguments forproxy-set-header
or something like that. Also I'm not sure if it's a bulletproof solution) - Configure your Helm chart installation values:
config:
"use-forwarded-headers": "true" # not true
"compute-full-forwarded-for": "true" # not true
Thanks Piotr for the hint :)
PS: There's also some other notes on the new fancy header Forwarded
that is supposed to solve the whole mess, but apparently Nginx doesn't fully support it yet.
PSS: I didn't spend time figuring out why the exception is thrown or how to prevent it. That's probably for another day :)