客户端IP获取

概念

X-Forwarded-For

X-Forwarded-For标准格式如下:

X-Forwarded-For: client1, proxy1, proxy2

从标准格式可以看出,X-Forwarded-For头信息可以有多个,中间用逗号分隔,第一项为真实的客户端ip,剩下的就是曾经经过的代理或负载均衡的ip地址,经过几个就会出现几个。

X-Forwarded-For和X-Real-IP

  1. X-Forwarded-For是用于记录代理信息的,每经过一级代理(匿名代理除外),代理服务器都会把这次请求的来源IP追加在X-Forwarded-For

    X-Real-IP一般只记录真实发出请求的客户端IP

  2. X-Forwarded-For在正向(如squid)反向(如nginx)代理中都是标准用法,

    而正向代理中是没有X-Real-IP相关的标准的,也就是说,如果用户访问你的 nginx反向代理之前,还经过了一层正向代理,你即使在nginx中配置了X-Real-IP,取到的也只是正向代理的IP而不是客户端真实IP

  3. 大部分nginx反向代理配置文章中都没有推荐加上X-Real-IP ,而只有X-Forwarded-For,因此更通用的做法自然是取X-Forwarded-For

  4. 多级代理很少见,只有一级代理的情况下二者是等效的

  5. 如果有多级代理,X-Forwarded-For效果是大于X-Real-IP的,可以记录完整的代理链路

相关请求头

  • X-Forwarded-For :这是一个 Squid 开发的字段,只有在通过了HTTP代理或者负载均衡服务器时才会添加该项。

    格式为X-Forwarded-For:client1,proxy1,proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器ip。现在大部分的代理都会加上这个请求头。

  • Proxy-Client-IP/WL- Proxy-Client-IP :这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。

  • HTTP_CLIENT_IP :有些代理服务器会加上此请求头。
  • X-Real-IP :Nginx代理一般会加上此请求头。

后端能获取到的三个IP地址

1
2
3
request.getHeader("X-Forwarded-For");
request.getHeader("X-Real-IP");
request.getRemoteAddr();

获取IP的接口

https://www.ip.cn/api/index?ip=&type=0

返回的数据格式

1
{"rs":1,"code":0,"address":"中国  河南省 郑州市 电信","ip":"123.53.36.16","isDomain":0}

JSONP调用

http://whois.pconline.com.cn/ipJson.jsp?callback=IPCallBack

返回的数据

1
if(window.IPCallBack) {IPCallBack({"ip":"123.53.36.16","pro":"河南省","proCode":"410000","city":"郑州市","cityCode":"410100","region":"","regionCode":"0","addr":"河南省郑州市 电信","regionNames":"","err":""});}

Nginx代理获取到的IP

假如我们的请求经过如下反向代理

请求 => proxy1 => proxy2 => proxy3 => 后端服务

假如

  • 请求的IP为6.6.6.6
  • proxy1的IP为1.1.1.1
  • proxy2的IP为2.2.2.2
  • proxy3的IP为3.3.3.3
  • 后端服务的IP为8.8.8.8

$remote_addr获取到的地址

proxy1:6.6.6.6
proxy2:1.1.1.1
proxy3:2.2.2.2

结果

只有第一个代理能获取到真实的IP

$proxy_add_x_forwarded_for获取到的地址

proxy1:6.6.6.6
proxy2:6.6.6.6,1.1.1.1
proxy3:6.6.6.6,1.1.1.1,2.2.2.2

proxy1、2、3 的配置中都加上:

1
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

结果

获取到的IP中第一个都是真实的IP

但客户端请求头中人为添加:X-Forwarded-For=192.168.1.1,192.168.1.2,再看看结果:

proxy1:192.168.1.1,192.168.1.2,6.6.6.6
proxy2:192.168.1.1,192.168.1.2,6.6.6.6,1.1.1.1
proxy3:192.168.1.1,192.168.1.2,6.6.6.6,1.1.1.1,2.2.2.2

结果

第一个IP都不是真实的IP了

怎么获取真实的IP

使用 X-Forwarded-For + realip模块

可以使用nginx的 ngx_http_realip_module 模块,从 X-Forwarded-For 或其他属性中提取真实IP。此处以 X-Forwarded-For 结合该模块为例子,需要做两件事:

  • 一是请求途径的各代理需要设置

    1
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  • 二是利用 realip 模块获取真实IP

这里proxy3的部分配置(proxy3将请求直接转发到后端服务),如下:

1
2
3
4
5
6
7
server {
location / {
set_real_ip_from 1.1.1.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
}
}
  • set_real_ip_from:表示从何处获取真实IP(解决安全问题,只认可自己信赖的IP),可以是IP或子网等, 可以设置多个set_real_ip_from。
  • real_ip_header:表示从哪个header属性中获取真实IP
  • real_ip_recursive:递归检索真实IP,若从 X-Forwarded-For 中获取,则需递归检索;若像从X-Real-IP中获取,则无需递归。

基于上一步的测试数据,试验结果:

$remote_addr获取到的地址

proxy1:6.6.6.6
proxy2:1.1.1.1
proxy3:6.6.6.6

$proxy_add_x_forwarded_for获取到的地址

proxy1:192.168.1.1,192.168.1.2,6.6.6.6
proxy2:192.168.1.1,192.168.1.2,6.6.6.6,1.1.1.1
proxy3:192.168.1.1,192.168.1.2,6.6.6.6,1.1.1.1,2.2.2.2

此时,proxy3 的 $remote_addr 已经拿到了客户端的真实IP 36.157.229.110,然后 proxy3 将 remote_addr 传递到后端服务中去。

后端获取

1
request.getRemoteAddr();

使用 X-Forwarded-For + 重置设置

由于客户端可以自行传递X-Forwarded-For,因此,可以在第一个代理处重置其值,达到忽略客户端传递的X-Forwarded-For的效果。

在 proxy1 中进行如下配置:

1
proxy_set_header X-Forwarded-For $remote_addr;

后端代码

1
2
3
4
5
6
7
8
9
10
String ip = request.getHeader("X-Forwarded-For");
if(StringUtils.isNotEmpty(ip) && !"unKnown".equalsIgnoreCase(ip)){
//多次反向代理后会有多个ip值,第一个ip才是真实ip
int index = ip.indexOf(",");
if(index!=-1){
return ip.substring(0,index);
}else{
return ip;
}
}

使用 X-Real-IP

由于proxy1的 $remote_addr 是客户端真实IP,因此在 proxy1 中将X-Real-IP的值设置为 $remote_addr 即可。

1
proxy_set_header X-Real-IP $remote_addr;

结果为:

$http_x_real_ip获取到的地址

proxy1:-
proxy2:6.6.6.6
proxy3:6.6.6.6

proxy1 中设置了X-Real-IP的值,proxy2、proxy3日志中可以看到该值

后端获取

1
request.getHeader("X-Real-IP");

仅后端判断

仅后端判断,如果人为添加:X-Forwarded-For=192.168.1.1,192.168.1.2这样的头,是没法获取真实的IP的,但是实际使用中我们可忽略,不确定代理的层级可使用如下代码获取

Nginx代理中配置

1
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

Java获取

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
/**
* 获取客户端ip地址
* @param request
* @return
*/
public static String getRealIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if( ip.indexOf(",")!=-1 ){
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}

附录

Nginx可用变量

http://nginx.org/en/docs/http/ngx_http_core_module.html#variables

搜索 Embedded Variables