问题描述
58站点在使用HTTP协议访问时,后端tomcat使用X-Forwarded-For这个参数来获取客户端真实IP。 但改用HTTPS协议后,后端tomcat无法通过X-Forwarded-For来获取客户端IP,X-Forwarded-For的值为null 。
问题排查
客户端和后端服务中间是运维统一接入的Nginx代理层,而HTTPS只到Nginx层即卸载证书,Nginx向后端转发请求使用HTTP协议,如下图所示
客户端请求无论是HTTP还是HTTPS协议都会以相同的方式把X-Forwarded-For的值传递给后端(HTTP和HTTPS的Nginx配置相同,只是多加了证书相关配置),实际测试发现,部分后端获取X-Forwarded-For值为null。
经过对比发现,获取不到X-Forwarded-For是因为tomcat的server.xml配置文件中多了以下配置。
Tomcat-server.xml配置:
在Engine段添加:
<span style=""><<span>Valve</span> <span>className</span>=<span>"org.apache.catalina.valves.RemoteIpValve"</span> </span>
<span><span>remoteIpHeader</span>=<span>"X-Forwarded-For"</span> </span>
<span><span>protocolHeader</span>=<span>"X-Forwarded-Proto"</span> </span>
<span><span>protocolHeaderHttpsValue</span>=<span>"https"</span></span>
<span>/></span>
该配置的作用是为了将用户真实的访问协议(HTTPS)通过X-Forwarded-Proto传给给tomcat,以保证后端处理完用户请求返回给用户的协议也是HTTPS,而不是获取Nginx访问的HTTP协议。
为配合此配置的传递功能,Nginx增加如下配置:
location 段:
<span><span>proxy_set_header</span> HTTPS-Tag <span>"HTTPS"</span>; <span>#设置HTTPS_TAG标识,方便程序认读</span></span>
<span><span>proxy_set_header</span> X-Forwarded-Proto <span>$scheme</span>; <span>#设置协议头传递变量</span></span>
在58站点进行HTTPS改造项目中,为了让后端都可以正常获取用户协议,因此在默认模板中统一添加了此配置。 在上线时,如果使用的是非全量包,上线过程会自动从模板中拉取server.xml配置文件。 所以导致了这部分站点获取X-Forwarded-Proto值时出现了问题。
问题原因
参考tomcat官方解释:
https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/valves/RemoteIpValve.html
例如:
客户端IP: 140.211.11.130
nginx内网IP: 192.168.0.10
此配置将X-Forwarded-For里的值排除内网或者可信任IP后,把最原始的请求IP赋值给了tomcat的remoteaddr变量,所以导致X-Forwarded-For的值为null,可以理解为tomcat更智能的为我们把用户IP转换到了remoteaddr变量了,而不再是直接访问tomcat的Nginx IP。
解决方案
1. 用户可以根据服务自身需求自定义tomcat的server.xml文件,并将配置放到全量包中上线,避免每次拉取默认配置。
2. tomcat默认模板添加此标准配置:
<span style=""><<span>Valve</span> <span>className</span>=<span>"org.apache.catalina.valves.RemoteIpValve"</span> </span>
<span><span>remoteIpHeader</span>=<span>"X-Forwarded-For"</span></span>
<span><span>protocolHeader</span>=<span>"X-Forwarded-Proto"</span></span>
<span><span>protocolHeaderHttpsValue</span>=<span>"https"</span></span>
<span>/></span>
3. Nginx改进更通用的传递方式。 除了X-Forwarded-For变量外,增加x-Real-IP变量,同时将两个变量的值都传递给后端,后端tomcat可以根据具体需求选择不同的变量来获取客户端真实IP。 具体配置如下:
#### 获取用户IP ####
set $remote_address $http_x_forwarded_for;
if ( $remote_address !~ "[0-9]" ) {
set $remote_address $remote_addr;
}
#### ----------- ####
location / {
proxy_pass http://$host_pass;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_address;
proxy_set_header X-Real-IP $remote_address;
proxy_set_header HTTPS-Tag "HTTP";
proxy_set_header X-Forwarded-Proto $scheme;
}
后续思考
客户端IP是站点日志中最为重要的部分,很多网站通过客户端IP分析用户分布,进行数据统计。
HTTP协议下获取客户端真实IP是通过X-Forwarded-For变量(以下简称XFF)。
X-Forwarded-For 格式:
X-Forwarded-For:client, proxy1, proxy2
XFF中记录的每个IP是用户从自己出口IP到服务器间,网络上经过的各种代理,最开始的是离服务端最远的设备IP,然后是每一级代理IP。
XFF的IP是可以随意伪造的:
curl http://10.9.XXX.XXX/api/v1/request/ip/address/ -H 'host:xff.58.com' -H 'x-forwarded-for:1.1.1.1,2.2.2.2,101.126.1.1'
后端返回的结果都是获取我伪造的IP
因此,要获取客户端IP,不能盲目的相信请求头里传递过来的值。
用户统计分析
地域判断
风险控制
对于线上请求,大部分的请求都是真实有效的,可以通过XFF的值来取到用户的IP。
但对于风险控制来讲,往往需要通过对IP及其他因素进行判断来限制访问。 可以伪造的XFF不是一个合理的取值。 应该使用真实连接nginx的IP进行限制。
业务同学可以根据实际需求,从请求头里获取最正确的IP。
Nginx相关配置参数及解释:
将请求Nginx的remoteIP赋值给XFF,并传给后端集群
proxy_set_header X-Forwarded-For $remote_addr;
将请求Nginx的remoteIP添加到XFF最后,并传给后端集群
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
为了防止XFF为空,额外增加X-Real-IP进行补充
proxy_set_header X-Real-IP $proxy_add_x_forwarded_for;