Java获取客户端IP

业务背景:

一些业务系统,一般有统计客户端IP来源,或者限定等拦截、或者白名单、黑名单等需求,这个时候就需要获取客户端真实的IP。
实际应用中发现,部分业务,获取的客户端 ip 地址不低,写法不统一,nginx 配置不一致。

具体场景:

1、未经过代理,直接访问服务器

  • 客户端请求信息都包含在HttpServletRequest中,对于第一种访问方式可以通过getRemoteAddr()方法获得客户端真实IP,这种方法在大部分情况下都是有效的。
    //request.getRemoteAddr(): 192.1.1.1
    代码示例:
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地址了

2、经过多级代理,到达服务器

  • 经过代理以后,由于在客户端和服务之间增加了中间层,因此服务器无法直接拿到客户端的IP,服务器端应用也无法直接通过转发请求的地址返回给客户端。
  • 但是在转发请求的HTTP头信息中,增加了X-FORWARDED-FOR等信息。用以跟踪原有的客户端IP地址和原来客户端请求的服务器地址。
  • 这样就可以通过x-forwarded-for获得转发后请求信息。当客户端请求被转发时,IP将会追加在其后并以英文逗号隔开,例如:192.168.1.110, 192.168.1.120, 192.168.1.130
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”);

3、需要支持内网外网判断的场景

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;”。(此时 remoteaddr;。(此时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

测试

本地环境部署:

1、关闭 vpn:

本地请求,获取 ip 为 127.0.0.1
服务器日志:

本地 ifconfig:

结论:没有代理,获取 127.0.0.1

2、开启 vpn

本地请求,获取 ip 为 127.0.0.1

3、其他人开启 vpn 本地请求

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 无关

测试环境部署

1、关闭 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:

2、开启 vpn

本地请求, 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

1、关闭 vpn

访问不了 cs 服务,网络不通

2、开启 vpn

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 生产日志打印,

正式环境部署
这个还没有上线,待定

最佳实践

1、代理层面处理

对于多级代理 ,一般最好遵循 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;”。(此时 remoteaddr;。(此时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一定真实吗?

2、具体代码

目前可以使用 或者后续增加功能,封装在
工具包,目前 信息管理组 内部用。 http://git.100credit.cn/rd_rdj/brkit
大家可以引入使用 com.br.kit.web.IpKit#getRequestIp

3、工具类示例代码

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;
}
  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值