记一次获取客户端IP不正确的情况

前言

之前有测试同学反馈他请求的转发服会分配非中国区。后面我们查了一下,发现我们程序获取的客户端 ip 不是真正的客户端ip,而是负载均衡服务器的ip 或者是 cdn 服务端的ip (反正就是反向代理服务器的ip)。
因此我们查看了一下获取 ip 的方法 (golang) :

1
2
3
4
5
6
7
8
9
//获取客户端ip, nginx代理传入的是 x-real-ip
func ClientIp(r *http.Request) string {
ip := r.Header.Get("X-Real-Ip")
if ip == "" {
s := strings.Split(r.RemoteAddr, ":")
ip = s[0]
}
return ip
}

发现这边只优先获取 x-real-ip, 如果找不到,那么就获取 remote addr

分析

但是这样会有一个问题,因为 x-real-ip 其实就是实际上跟我们的业务服务器请求的那一台服务器,如果我们的服务有用了负载均衡或者动态加速服务的话。那么往往最后跟业务服务器进行请求的服务器就不是用户的客户端,而是负载均衡服务器或者是动态CDN服务器。因此就会导致我们获取的客户端ip就不对,就不是用户的真正的客户端ip了。
首先我们来看一下 nginx 的转发配置, 这样是为了记录完整的代理过程:

1
2
3
4
5
6
location /p20/ {
proxy_pass http://p20/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

可以看到会将 $remote_addr 赋给 x-real-ip 这个头部。 而 x-real-ip 是不能被篡改的。而对于 nginx 转发的服务,也不能直接取请求的 remote addr,因为这时候就会取到本机的 nginx 的服务的 ip 和端口。
我们来看个例子: 这边有个接口:

1
2
3
4
5
6
7
8
9
10
func (t *Test) Ip() revel.Result {
res := Res{}
data := Data{}
data.REMOTEADDR = t.Request.RemoteAddr
data.XREALIP = t.Request.Header.Get("X-Real-IP")
data.XFORWARDFOR = t.Request.Header.Get("X-Forwarded-For")
res.Code = 1
res.Data = data
return utils.EchoResult(t.Controller, res)
}

然后我们请求一下,看看输出结果:

1
2
3
4
5
6
7
8
9
10
{
code: 1,
msg: "",
data: {
x_real_ip: "118.xx.19.127",
x_forward_for: "125.xx.202.250, 118.xx.19.127, 118.xx.19.127",
remote_addr: "127.0.0.1:35971"
},
extra: null
}

因为我们这个服务有用到了腾讯云的 clb 负载均衡服务。 所以可以看到 x-real-ip 其实就是最后一台负载均衡服务器的 ip。并不是真实的客户端ip。而 remote addr 也不是客户端ip,而是 nginx 本机服务的 ip 和 端口。只有 x-forward-for 这个头部的第一个ip地址才是真实的客户端ip。

X-Forwarded-For

X-Forwarded-For 是一个可叠加的过程,后面的代理会把前面代理的 IP 加入X-Forwarded-For,类似于python的列表append的作用.
Remote Address代表的是当前HTTP请求的远程地址,即HTTP请求的源地址。HTTP协议在三次握手时使用的就是这个Remote Address地址,在发送响应报文时也是使用这个Remote Address地址。因此,如果请求者伪造Remote Address地址,他将无法收到HTTP的响应报文,此时伪造没有任何意义。这也就使得Remote Address默认具有防篡改的功能。
在一些大型网站中,来自用户的HTTP请求会经过反向代理服务器的转发,此时,服务器收到的Remote Address地址就是反向代理服务器的地址。在这样的情况下,用户的真实IP地址将被丢失,因此有了HTTP扩展头部X-Forward-For。当反向代理服务器转发用户的HTTP请求时,需要将用户的真实IP地址写入到X-Forward-For中,以便后端服务能够使用。由于X-Forward-For是可修改的,所以X-Forward-For中的地址在某种程度上不可信。所以,在进行与安全有关的操作时,只能通过Remote Address获取用户的IP地址,不能相信任何请求头。
当然,在使用 nginx 等反向代理服务器的时候,是必须使用X-Forward-For来获取用户IP地址的(此时Remote Address是nginx的地址),因为此时X-Forward-For中的地址是由 nginx 写入的,而 nginx 是可信任的。不过此时要注意,要禁止web对外提供服务。

最后修改

所以最后这个方法要改成要优先取 X-Forward-For 的值。没有再取 x-real-ip, 再没有,再取 remote address

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
// 获取客户端 ip
// X-Forwarded-For : client ip, proxy ip, proxy ip ..., 理论上应该取逗号分隔后的第一个 ip
// X-Real-Ip : 最接近 nginx 的 ip, 如果请求经过多个代理转发,那么获取到的最后一个代理服务器的 ip
// remote addr : go net 包取的 ip, 如果经过 nginx upstream 反向代理到本机的 go 程序,则会取到 127.0.0.1
func ClientIp(r *http.Request) string {
ip := ""

// X-Forwarded-For
xForwardedFor := r.Header.Get("X-Forwarded-For")
if xForwardedFor != "" {
proxyIps := strings.Split(xForwardedFor, ",")
if len(proxyIps) > 0 {
ip = proxyIps[0]
log.Info(ip, " hit x-forwarded-for")
return ip
}
}

// X-Real-IP, X-Real-Ip
ip = r.Header.Get("X-Real-IP")
if ip == "" {
ip = r.Header.Get("X-Real-Ip")
}
if ip != "" {
log.Info(ip, " hit x-real-ip")
return ip
}

// Remote Addr
s := strings.Split(r.RemoteAddr, ":")
if len(s) > 0 {
ip = s[0]
log.Info(ip, " hit remote addr")
}
return ip
}

最后再附上 PHP 程序的对应方法:

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
/**
* 获取浏览器的ip地址
*/
public static function getClientIP()
{
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
} elseif (getenv('HTTP_X_FORWARDED_FOR')) {
$ip = getenv('HTTP_X_FORWARDED_FOR');
} elseif (getenv('HTTP_CLIENT_IP')) {
$ip = getenv('HTTP_CLIENT_IP');
} elseif (getenv('REMOTE_ADDR')) {
$ip = getenv('REMOTE_ADDR');
} else {
$ip = '';
}

if (strpos($ip, ',') !== false) {
$ips = explode(',', $ip);
$ip = trim($ips[0]);
}

return $ip;
}

正确设置转发 X-Real-IP 字段

后面发现有些框架有自带获取 clientIp 的工具库,这些工具库的获取的顺序有可能是先获取 X-Real-IP, 再获取 X-Forwarded-For, 就跟我们没有改之前的一样, 这时候 $remote_addr 在客户端有走代理的情况下 (比如翻墙行为),就会变成是代理服务器的 ip,而不是客户端的真实 ip

1
proxy_set_header  X-Real-IP  $remote_addr;

这时候如果不想去改框架自带的工具库方法的话,那么就要正确的设置 X-Real-IP 的值, 这时候就两种方式:

而且 X-Real-Ip 不设置时,nginx 原样转发,有可能请求端可能通过设置这个头来伪造 IP ,在一些和区域严格相关(比如风控)的业务上有风险。还是建议设置上,将其设置为原始的客户端 IP 而不是代理的上一跳 IP。

1. $realip_remote_addr

1
proxy_set_header X-Real-IP $realip_remote_addr;

改成设置这个 $realip_remote_addr, 这个就是客户端的真实 ip 了,不过这个参数需要加载 nginx 模块 --with-http_realip_module 才能取到

2. 自定义获取 $clientRealIp

这种方式就是在 nginx 配置文件中定义一个 $clientRealIp 的真实 ip,然后就可以用了

1
2
3
4
5
# 写在 nginx.conf 中
map $http_x_forwarded_for $clientRealIp {
"" $remote_addr;
~^(?P<firstAddr>[0-9\.]+),?.*$ $firstAddr;
}

这段代码的目的是提取出客户端的真实IP地址。如果请求头中包含X-Forwarded-For字段(通常在客户端通过代理服务器连接时会有这个字段),则使用这个字段中的第一个IP地址作为客户端的真实IP地址;否则,使用$remote_addr作为客户端的真实IP地址。

然后就可以在配置代理转发的时候,直接用了

1
proxy_set_header X-Real-IP $clientRealIp;

参考

HTTP 请求头中的 X-Forwarded-For,X-Real-IP
X-Forwarded-For的一些理解