Spring 获取客户端真实IP地址

 

相信很多人都遇到过,使用HttpServletRequest的getRemoteAddr()方法获取客户端IP得到的是127.0.0.1或0:0:0:0:0:0:0:1。出现这种情况多半是因为:

  1. 本地访问
  2. 设置了反向代理,通过反向代理访问

本地访问好说,不用解决,是正常现象。如果是反向代理,需要获取http头X-Forwarded-For(前提是有正确设置)。

String ip = request.getHeader("x-forwarded-for");

if (StringUtils.isEmpty(ip)) {
    ip = request.getRemoteAddr();
}

上面代码可以解决反向代理的问题,不过代码不够健壮,存在安全隐患。想知道为啥,请接着往下看。

RemoteAddress

为什么会获取到本地地址呢?这还得了解一下RemoteAddr。我们来看一下HttpServletRequest中对getRemoteAddr的注释

Returns the Internet Protocol (IP) address of the client or last proxy that sent the request. For HTTP servlets, same as the value of the CGI variable REMOTE_ADDR.

通俗点说,getRemoteAddr()获取到的是直接与Server连接的Client的IP地址。本地访问或反向代理服务器刚好和Server是在同一个地方,就会出现remote addr刚好都是12.0.0.1。

这就解释了为什么反向代理会导致127.0.0.1的结果。

X-Forwarded-For

X-Forwarded-For简称为XFF,是一个非标准的Http头。它主要的作用维基百科说的非常清楚。

X-Forwarded-For(XFF)是用来识别通过HTTP代理或负载均衡方式连接到Web服务器的客户端最原始的IP地址的HTTP请求头字段。- 维基百科

从上面的例子可以看出通过getRemoteAddr只能获取到直接与服务器建立连接的设备的ip地址。如果使用了代理服务器,那么将无法检测到源设备的IP地址,所以需要一种机制告诉服务器,因此就有了X-Forwarded-For。

Nginx是这样配置的

server {
   listen 5000;
   server_name 192.168.10.10;
   location / {
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
       proxy_pass http://192.168.10.1:8104/;
   }
}

配置了后,Nginx会自动把源ip放在X-Forwarded-For头里面。如果经历了多级代理,最后一个反向代理会记录前面所有的ip,顺序如下:

X-Forwarded-For: $origin,proxy1,proxy2,proxy3

上面说了,X-Forwarded-For是一个普通的Http Header,这就存在一个隐患: Http头可以随便更改。

现在我们有2台机器,分别为192.168.10.1(client), 192.168.10.10(server),client通过下面方式请求server,观察一下结果。

直接访问:

{
	"remoteAddr": "192.168.10.1",
	"x-forwarded-for": null
}

反向代理(Server同时也是一台反向代理服务器)

{
  "x-forwarded-for": "192.168.10.1",
  "remoteAddr": "127.0.0.1"
}

自定义Http头

curl --header "X-Forwarded-For: 8.8.8.8" http://192.168.10.10:8104/api/test
{
  "x-forwarded-for": "8.8.8.8",
  "remoteAddr": "192.168.10.1"
}

最后一次请求可以看到X-Forwarded-For变成了8.8.8.8。因此我们不能盲目信任客户端的Http头,想安全获取真正的IP地址,必须要确定getRemoteAddr()是我们信任的服务器地址。所以必须把所有的反向代理服务器以及负载均衡的ip记录在一个白名单列表,我们仅信任这个白名单的X-Forwarded-For头。

public static String getIpAddress(HttpServletRequest request) {
    String remoteAddr = request.getRemoteAddr();

    /* 信任来自白名单的远程反向代理服务器 */
    if (whileList.contains(remoteAddr)) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip != null) {
            /* 经过多重反向代理会带有多个反向代理服务器ip */
            String[] ipArr = ip.split(",");

            return ipArr[0];
        }
    }

    return remoteAddr;
}