Tomcat从Nginx服务器获取客户端IP

这篇文章的信息量有点多,因为需要从这一点衍生出不少容易踩的坑。先说说标题中的问题,主要是使用了Nginx做反向代理服务器,然后后端Tomcat服务器无法获取用户真实的IP地址,使用httpServletRequest.getRemoteAddr()一直都是127.0.0.1。我们先来说一说为什么是这样?

为什么getRemoteAddr()一直是127.0.0.1?

首先需要明白整体的架构图,如下:

可以看到,用户访问网站的时候是通过Nginx做了一层反向代理,请求被分发到后端的某一台Tomcat服务器。其中,用户请求到Nginx的时候IP地址是202.114.78.189,而Nginx反向代理到Tomcat服务器的时候是127.0.0.1。我们知道在网络中可能会存在一些中间结点,比如代理服务器,而这里的代理服务器就是Nginx。

好,Nginx能获取到用户的IP地址,这个没问题。那么,为什么Tomcat不能获取到用户的真实地址呢?那是因为Tomcat的用户地址就是Nginx啊,Tomcat并没有直接和用户连接,所以这里我们就不难理解,Tomcat通过方法httpServletRequest.getRemoteAddr()只能够获取到Nginx的真实地址,而这个架构图中Nginx服务器和Tomcat服务器又处于同一个局域网,所以为127.0.0.1很正常。那么如何来解决这个问题呢?

使用Nginx透传用户IP的请求头信息

其实前面的几篇文章已经将Nginx的这个配置给出了,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80;

server_name acs.qinjiangbo.com;
access_log /var/log/nginx/access.log main;
location / {
proxy_pass http://127.0.0.1:8080/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

可以看到上面的例子中使用了proxy_set_header命令,直观意思就是代理设置头方法,正是我们前面提到过的。这里解释一下$host$remote_addr以及$proxy_add_x_forwared_for三个变量的意思。

  • $host这个就是客户端的主机名称。
  • $remote_addr这个就是客户端的主机地址。
  • $proxy_add_x_forwared_for这个就是在转发请求的时候加上一个X-Forwared_For头信息,而在这个头信息里面会加上客户端的真实IP地址。

这样后端就能通过这几个参数获取到客户端的真实地址了。但是一般我更加喜欢使用第二个配置,就是proxy_set_header X-Real-IP $remote_addr;这里,我需要在Tomcat里面写上一个能够读取X-Real-IP请求头信息的方法,这样就可以拿到真实客户端地址。

问题解决方案

使用装饰器模式来封装HttpServletRequest类,可以达到更改某一个方法的行为的目的。也就是说我们需要使用一个能包装HttpServletRequest类的包装类,在这个包装类中,我们重写getRemoteAddr()方法。关于装饰器模式,我不想在这里再一次说了,有需要的可以看一下我的这篇博客《设计模式学习之装饰者模式》,需要注意的一点就是装饰者和被装饰者需要实现共同的接口,不然被装饰者没法被装饰者替换嘛!

仔细看一下ServletRequest的类图我们就可以知道要使用哪一个类了。类图如下:

我们可以看到,HttpServletRequest接口继承自ServletRequest接口。ServletRequestWrapper类实现了ServletRequest接口,而HttpServletRequestWrapper类继承自ServletRequestWrapper类,所以直观上来看,HttpServletRequestWrapper类是可以作为HttpServletRequest类的装饰者的。我们需要自己写一个类继承自HttpServletRequestWrapper类,然后实现自己的方法逻辑。

实现RequestWrapper类

我实现的包装类叫做RequestWrapper,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.qinjiangbo.util;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

/**
* @date: 11/12/2017 5:50 PM
* @author: [email protected]
* @description:
* 装饰器类,主要用于处理Tomcat本地获取客户端IP一直都是127.0.0.1的情况
*/
public class RequestWrapper extends HttpServletRequestWrapper {

/**
* X-Real-IP头名称
*/
private static final String X_REAL_IP = "X-Real-IP";

/**
* Constructs a request object wrapping the given request.
*
* @param request
* @throws IllegalArgumentException if the request is null
*/
public RequestWrapper(HttpServletRequest request) {
super(request);
}

@Override
public String getRemoteAddr() {
// 直接获取X-Real-IP头的信息
String realIP = getHeader(X_REAL_IP);
if (realIP != null && !"".equals(realIP)) {
return realIP;
}
return super.getRemoteAddr();
}
}

拦截器和过滤器的区别

实现了RequestWrapper类以后需要使用到它呀,我们希望原有的httpServletRequest.getRemoteAddr()方法不受影响,因为我这边通过RequestWrapper类已经实现了,希望后面的相同方法都能直接使用,我之前想到过SpringMVC的拦截器,但是这里就引出一个问题,就是过滤器和拦截器的区别。

简单地来说就是拦截器不能改变request和response,而过滤器能改变request和response,也就是说我们只有选择使用过滤器了。在知乎上看到一个很有意思的回答“spring中HttpServletRequestWrapper装饰者模式是如何理解的? - ScienJus的回答 - 知乎”,我觉得非常经典:

拦截器模式

1
2
3
4
5
6
7
8
9
10
void run () {
Request request = new Request();
preHandle(request);
service(request);
}

void preHandler(Request request) {
//在这里修改Request的引用,不会影响到service方法的request
request = new RequestWrapper(request);
}

过滤器模式

1
2
3
4
5
6
7
8
9
10
void run () {
Request request = new Request();
doFilter(request);
}

void doFilter(Request request) {
//在这里修改Request的引用,会影响到service方法的request
request = new RequestWrapper(request);
service(request);
}

可以看到拦截器模式中,preHandle方法修改了request对象的引用,但是service(request)并没有任何变化,因为我们知道方法外的request对象引用并没有发生变化。而在过滤器模式中,doFilter方法也修改了request对象的引用,但是service(request)方法中能够获取到的都是装饰过的request对象。

实现HttpRequestFilter类

我们需要实现自己的Filter类,通过这个过滤器将自己加工后的request注入到流程中去,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.qinjiangbo.util;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
* @date: 11/12/2017 5:57 PM
* @author: [email protected]
* @description:
*/
public class HttpRequestFilter implements Filter {

@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
// 替换一下
request = new RequestWrapper((HttpServletRequest) request);
// 注入进去了
chain.doFilter(request, response);
}

@Override
public void destroy() {

}
}

web.xml配置注意事项

我以为把上面的这些坑踩完了就没事儿了呢,没想到啊,在web.xml中又存在一个坑,就是Filter的URL映射模式。我以为和Servlet一样都是/,没想到这样添加进去并不行。下面系统总结一下过滤器的url-pattern相关问题。

参考了《Web.xml中设置Servlet和Filter时的url-pattern匹配规则》这篇文章相关儿内容,它从两个方面进行了系统的阐述,在这里我们重点关注url-pattern匹配规则

url-pattern匹配规则

url-pattern匹配原则就是找到唯一一个最适合的Servlet。Servlet匹配规则有以下几点:

  • 精确路径匹配,模式/*/test都存在的时候,会优先匹配/test
  • 最长路径匹配,模式/test/*/test/a/*都存在的时候,会优先匹配/test/a/*
  • 扩展匹配,容器会根据匹配的拓展匹配,比如*.do/test/*.do是不合法的模式。
  • 默认匹配,如果容器前三中模式都没有匹配上,则会选择默认的Servlet来处理。

对于Filter,不会像Servlet那样只匹配一个Servlet,因为Filter的集合是一个链,所以只会有处理的顺序不同,而不会出现只选择一个Filter。Filter的处理顺序和filter-mapping在web.xml中定义的顺序相同。

最重要的一句话来了,一般在做全路径匹配的时候,Servlet的url-pattern是/,而Filter的url-pattern是/*注意后面是多了一个*号的,我就是在这个上面踩了大坑。

总结

在这一次的踩坑中,收获了很多,了解了Filter和Servlet的一些关联与区别,另外也了解到了如何在不改变原有代码设计的基础上增加新的行为特征。再次重温了一下装饰者模式,收获也很大。

鸣谢-参考文章

感谢下面两位提供的一些思路,虽然没法当面感谢,但是还是在此谢过了!

分享到