一些业务系统,一般有统计客户端IP来源,或者限定等拦截、或者白名单、黑名单等需求,这个时候就需要获取客户端真实的IP。
实际应用中发现,部分业务,获取的客户端 ip 地址不低,写法不统一,nginx 配置不一致。
public String getRemortIP(HttpServletRequest request) {
if (request.getHeader("x-forwarded-for") == null) {
return request.getRemoteAddr();
}
return request.getHeader("x-forwarded-for");
}
但是在通过了Apache,Squid,nginx等反向代理软件就不能获取到客户端的真实IP地址了
public static String getReqIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip) && StringUtils.contains(ip, ",")) {
// 多次反向代理后会有多个IP值,第一个为真实IP。
ip = StringUtils.substringBefore(ip, ",");
}
// 处理localhost访问
if (StringUtils.isBlank(ip) || "unkown".equalsIgnoreCase(ip) || StringUtils.split(ip, ".").length != 4) {
try {
InetAddress inetAddress = InetAddress.getLocalHost();
ip = inetAddress.getHostAddress();
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
return ip;
}
首先,我们获取 X-Forwarded-For 中第0位的IP地址,它就是在HTTP扩展协议中能表示真实的客户端IP。 客户端访问经过转发,IP将会追加在其后并以逗号隔开。 如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串Ip值,究竟哪个才是真正的用户端的真实IP呢?答案是取X-Forwarded-For中第一个并且不是unknown的有效IP字符串。 如:X-Forwarded-or:192.168.1.110, 192.168.1.120, 192.168.1.130, 192.168.1.100 用户真实IP为:192.168.1.110
request.getHeader(“x-forwarded-for”) : 192.168.1.110, 192.168.1.120, 192.168.1.130
如果 X-Forwarded-For 获取不到,就去获取X-Real-IP ,X-Real-IP 就是记录请求的客户端真实IP。跟X-Forwarded-For 类似。
request.getHeader(“X-Real-IP”) : 10.47.103.13
X-Real-IP 获取不到,就依次获取Proxy-Client-IP 、WL-Proxy-Client-IP 、HTTP_CLIENT_IP 、 HTTP_X_FORWARDED_FOR 。
request.getHeader(“Proxy-Client-IP”)
request.getHeader(“WL-Proxy-Client-IP”)
request.getHeader("HTTP_CLIENT_IP ")
request.getHeader(“HTTP_X_FORWARDED_FOR”)
最后获取不到才通过request.getRemoteAddr()获取IP,
request.getHeader(“x-forwarded-for”);
public static String getReqIp2(HttpServletRequest request) {
String headName = "X-Forwarded-For";
String ip = request.getHeader(headName);
log.info(headName + " ip:{}", ip);
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
headName = "X-Real-IP";
ip = request.getHeader(headName);
log.info(headName + " ip:{}", ip);
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
headName = "Proxy-Client-IP";
ip = request.getHeader(headName);
log.info(headName + " ip:{}", ip);
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
headName = "WL-Proxy-Client-IP";
ip = request.getHeader(headName);
log.info(headName + " ip:{}", ip);
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
headName = "HTTP_CLIENT_IP";
ip = request.getHeader(headName);
log.info(headName + " ip:{}", ip);
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
headName = "HTTP_X_FORWARDED_FOR";
ip = request.getHeader(headName);
log.info(headName + " ip:{}", ip);
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("getRemoteAddr ip:{}", ip);
}
String tempIP = null;
if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip) && StringUtils.contains(ip, ",")) {
String[] ipArray = ip.split(",");
for (String ip_one : ipArray) {
if (!isInnerIP(ip_one.trim())) {
tempIP = ip_one.trim();
break;
}
}
//如果多ip都是内网ip,则取第一个ip.
if (null == tempIP) {
tempIP = ipArray[0].trim();
}
ip = tempIP;
}
if (ip != null && ip.contains("unknown")) {
ip = ip.replaceAll("unknown,", "");
ip = ip.trim();
}
if (StringUtils.isBlank(ip) || "unkown".equalsIgnoreCase(ip) || StringUtils.split(ip, ".").length != 4) {
try {
InetAddress inetAddress = InetAddress.getLocalHost();
ip = inetAddress.getHostAddress();
log.info("getHostAddress ip:{}", ip);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
return ip;
}
public static boolean isInnerIP(String ipAddress) {
boolean isInnerIp;
long ipNum = getIpNum(ipAddress);
/**
私有IP:A类 10.0.0.0-10.255.255.255
B类 172.16.0.0-172.31.255.255
C类 192.168.0.0-192.168.255.255
当然,还有127这个网段是环回地址
**/
long aBegin = getIpNum("10.0.0.0");
long aEnd = getIpNum("10.255.255.255");
long bBegin = getIpNum("172.16.0.0");
long bEnd = getIpNum("172.31.255.255");
long cBegin = getIpNum("192.168.0.0");
long cEnd = getIpNum("192.168.255.255");
isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd) || ipAddress.equals("127.0.0.1");
return isInnerIp;
}
private static long getIpNum(String ipAddress) {
String[] ip = ipAddress.split("\\.");
long a = Integer.parseInt(ip[0]);
long b = Integer.parseInt(ip[1]);
long c = Integer.parseInt(ip[2]);
long d = Integer.parseInt(ip[3]);
return a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d;
}
private static boolean isInner(long userIp, long begin, long end) {
return (userIp >= begin) && (userIp <= end);
}
public static String getRequestIp() {
String ip = "";
try {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
ip = getReqIp2(request);
} catch (IllegalStateException e) {
log.error("getRequestIp error", e);
}
return ip;
}
相关请求头的解释:
X-Forwarded-For 记录一个请求从客户端出发到目标服务器过程中经历的代理,或者负载平衡设备的IP。这是由缓存代理软件 Squid 引入,用来表示 HTTP 请求端真实 IP,现在已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中。
X-Forwarded-For 请求头格式非常简单,就这样:
X-Forwarded-For: client, proxy1, proxy2
可以看到,XFF 的内容由「英文逗号 + 空格」隔开的多个部分组成,最开始的是离服务端最远的设备 IP,然后是每一级代理设备的 IP。
如果一个 HTTP 请求到达服务器之前,经过了三个代理 Proxy1、Proxy2、Proxy3,IP 分别为 IP1、IP2、IP3,用户真实 IP 为 IP0,那么按照 XFF 标准,服务端最终会收到以下信息:
X-Forwarded-For: IP0, IP1, IP2
X-Real-IP nginx代理一般会加上此请求头。
Proxy-Client-IP/WL- Proxy-Client-IP 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。
HTTP_CLIENT_IP 有些代理服务器会加上此请求头。
这些请求头(例如 X-Forwarded-For、X-Real-IP、 Proxy-Client-IP 等)可以帮助识别客户端真实IP地址,但也需要小心处理,因为它们可以被模拟或伪造,有时并不是绝对可信的。因此,在使用这些请求头获取IP地址时,需要根据实际情况做出适当的验证和过滤,以确保安全可靠地获取到客户端的真实IP地址。
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
风险: 用户可以通过自己设置请求头来伪造ip,比如用户在发起http请求是自己测试请求头x-forwarded-for:192.168.0.151。那么服务器通过x-forwarded-for获取到的第一个ip就是用户伪造的ip。
防止伪造方案:
情况1: 在只有1层nginx代理的情况下,设置nginx配置“proxy_set_header X-Forwarded-For
r
e
m
o
t
e
a
d
d
r
;
”。(此时
remote_addr;”。(此时
remotea?ddr;”。(此时remote_addr获取的是用户的真是ip)
情况2:在有多层反向代理的情况下,
1)设置“最外层”nginx配置和情况1一样“proxy_set_header X-Forwarded-For $remote_addr;”。
2)除了“最外层”之外的nginx配置“proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;”。
这样就防止了用户通过伪造请求头来伪造真实ip。后台只需要从x-forwarded-for请求头中取出第一个ip就是用户的真实ip。后面如果有多个ip,就是反向代理的ip
同理:X-Real-IP也差不多。
不同的是当只有1层nginx代理情况下只需配置“proxy_set_header X-Real-IP $remote_addr;”即可。
当有多层反向代理时,只在最外层代理设置“proxy_set_header X-Real-IP $remote_addr;”,如果在非最外层设置,则获取到的是反向代理机器的ip
本地请求,获取 ip 为 127.0.0.1
服务器日志:
本地 ifconfig:
结论:没有代理,获取 127.0.0.1
本地请求,获取 ip 为 127.0.0.1
curl -X GET “http://172.16.83.228:8290/api/ding/getRequestIp”
本地请求,获取 ip 为 172.16.80.168
1.本人本机请求 curl -X GET “http://127.0.0.1:8290/api/ding/getRequestIp” ,获取 IP 为127.0.0.1 ,请求机器的 ip,和 vpn 无关
2.本人本机请求 curl -X GET “http://172.16.83.228:8290/api/ding/getRequestIp” ,获取 IP 为172.16.83.228 ,请求机器的 ip,和 vpn 无关
3.他人本钱请求 curl -X GET “http://172.16.83.228:8290/api/ding/getRequestIp” ,获取 IP 为172.16.83.228 ,请求机器的 ip,和 vpn 无关
本地请求, curl -X GET “http://192.168.162.235:8290/api/ding/getRequestIp” 获取 IP 为172.16.83.228 ,请求机器的 ip
服务器日志:
tail -f /opt/SpringCloud/logs/cs/info.log
本地 ifconfig:
本地请求, curl -X GET “http://192.168.162.235:8290/api/ding/getRequestIp” 获取 ip 为 172.0.3.52,请求机器的 vpnip,和 vpn 有关系
服务器日志:
tail -f /opt/SpringCloud/logs/cs/info.log
本地 vpn,可以看到,获取的是 vpn ip
本地 ifconfig:
1.关闭 vpn,curl -X GET “http://192.168.162.235:8290/api/ding/getRequestIp” 获取 IP 为172.16.83.228 ,请求机器的 ip
2.开启 vpn,curl -X GET “http://192.168.162.235:8290/api/ding/getRequestIp” 获取 ip 为 172.0.3.52,请求机器的 vpn ip,和 vpn 有关系
cs-pre.100credit.cn : 10.100.123.74
访问不了 cs 服务,网络不通
curl -X GET “http://cs-pre.100credit.cn/api/ding/getRequestIp” 获取地址 10.100.123.87
服务器日志:
10.100.123.87 这个地址不是我想要的,和运维沟通后,是ha 的地址, 深入沟通后,这个是配置问题,nginx增加了这个配置,ok 了,目前是 cs 服务增加,后续需要单独申请
修改后请求
curl -X GET “http://cs-pre.100credit.cn/api/ding/getRequestIp”
可以看到 , X-Forwarded-For 带了了多个地址 X-Forwarded-For ip:172.0.3.52,10.100.123.87[traceId-d6b6255f-78ef-49e1-bdb1-1cd9575120b6]
第一个 172.0.3.52 vpn 地址, 第二个是 10.100.123.87 ha 地址
cas 案例
1 、联系运维,增加 nginx 参数 X-Forwarded-For
2、查看审计日志,修改 ok
ehr 案例
预发环境,审计日志获取的是 172.98.9.219
分为获取 host 和 ip ,host不作为区分,页面修改为 host 为 ip,或者 2 者共存 ,使用 ip 认定为真实的用户 ip
String host = request.getRemoteHost();
String ip = IpTool.getIp(request);
ehr生产环境 ,中间有一跳丢了,是因为加了个4层代理
client -> 106 -> 135 -> 170 -> 173 -> pod
client -> 办公网nginx -> idc nginx -> k8s haproxy -> k8s ingress -> pod
办公网nginx : add_xff = remote_addr = client_addr
idc nginx : add_xff = 上一跳的 add_xff + remote_addr = client_add + 办公网nginx
k8s haproxy : tcp proxy => 完全透传
k8s ingress : add_xff = 上一跳的 add_xff + remote_addr = client_add + 办公网nginx + k8s haproxy
pod 内部抓包
java 服务抓包
ehr 生产日志打印,
正式环境部署
这个还没有上线,待定
对于多级代理 ,一般最好遵循 X-Forwarded-For 设置规范 ,一层层跳转 ,和运维做好沟通,代码上处理优先获取 X-Forwarded-For 头
实际上,如果 nginx 在运维层设置正确,代码层面可以不做额外处理的
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
情况1: 在只有1层nginx代理的情况下,设置nginx配置“proxy_set_header X-Forwarded-For
r
e
m
o
t
e
a
d
d
r
;
”。(此时
remote_addr;”。(此时
remotea?ddr;”。(此时remote_addr获取的是用户的真是ip)
情况2:在有多层反向代理的情况下,
1)设置“最外层”nginx配置和情况1一样“proxy_set_header X-Forwarded-For $remote_addr;”。
2)除了“最外层”之外的nginx配置“proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;”。
参考:
Nginx 获取客户端真实IP $remote_addr与X-Forwarded-For
Nginx 之 X-Forwarded-For 中首个IP一定真实吗?
目前可以使用 或者后续增加功能,封装在
工具包,目前 信息管理组 内部用。 http://git.100credit.cn/rd_rdj/brkit
大家可以引入使用 com.br.kit.web.IpKit#getRequestIp
public static String getRequestIp() {
String ip="";
try {
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
ip = getIp(request);
} catch (IllegalStateException e) {
e.printStackTrace();
}
return ip;
}
public static String getIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
log.info("IpTool X-Forwarded-For ip:{}", ip);
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
log.info("IpTool X-Real-IP ip:{}", ip);
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (StringUtils.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
log.info("IpTool getRemoteAddr ip:{}", ip);
}
if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip) && StringUtils.contains(ip, ",")) {
// 多次反向代理后会有多个IP值,第一个为真实IP。
ip = StringUtils.substringBefore(ip, ",");
log.info("IpTool substringBefore ip:{}", ip);
}
// 处理localhost访问
if (StringUtils.isBlank(ip) || "unkown".equalsIgnoreCase(ip) || StringUtils.split(ip, ".").length != 4) {
try {
InetAddress inetAddress = InetAddress.getLocalHost();
ip = inetAddress.getHostAddress();
log.info("IpTool getLocalHost ip:{}", ip);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
return ip;
}