java获取客户端ip探索

3,577 阅读4分钟

开发诉求:我的系统要获取客户ip,记录到数据库

写法一

 ip=request.getRemoteAddr();

代码里使用 request.getRemoteAddr() 这个函数来获取用户的ip,这个时候由于http请求最后是由nginx向业务系统发起的,http请求里的remoteaddr获取到的将是nginx的ip,完全记录不到用户的真实ip,这种记录请求来源的方法只适合内部使用的系统没有经过反向代理

备注: request.getRemoteAddr() 这个函数并不从http请求头获取任何客户ip,而是基于TCP/IP网络协议栈来识别访问请求发起的来源ip的,当存在代理时,实际发往业务系统的请求是由代理发起的,所以你将获取到代理的ip


写法二

ip = request.getHeader("x-forwarded-for");

if (ip.length() > 15 && ip.indexOf(",")>0) {
    ip = ip.substring(0,ip.indexOf(","));
}

恭喜你,80%的可能性获取到了真实的客户ip 。

代码里使用获取http头里的x-forwarded-for变量的第一个ip作为客户真实ip,这种方式下,大概率情况下我们能获取到用户的真实ip,但是为什么说不是绝对的呢 

原因1 

x-forwarded-for是一个传递拼接性质的http头,也是一个非标准的http规范,当业务系统经过代理对外暴露服务时,代理获取到上一级访问者上送的x-forwarded-for值,然后再最后添加(,上一级访问者的ip)这个时候x-forwarded-for的值为(上上一级的ip,上一级访问者ip),那么我们获取第一个ip就能正确的获取到客户真实ip 但是这依赖于nginx代理时,正确的配置了proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;否则获取到的x-forwarded-for将为空,那么我们将无法准确获取客户端的真实ip 一般浏览器作为请求的第一个发送者,不会上送x-forwarded-for这个请求头,第一级nginx代理设置proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;时,nginx会自动获取它拿到的remote_addr赋值给x-forwarded-for,因为第一级nginx直接收到来自客户端的请求,这个时候remote_addr是准确的客户端真实ip,第一级nginx的产生的x_forwarded_for=第一级nginx的访问者的ip(客户真实ip) ,第二级nginx的产生的x_forwarded_for= 第一级nginx的访问者的ip(客户真实ip) , 第二级nginx的访问者的ip(第一级nginx的真实ip),这样依次类推,当然了,实际生产环境中,web和接口请求不会经过太多级别的代理,一般也就cdn一次。

原因2 

客户恶意上送x-forwarded-for ,客户端发起请求时,主动上送一个x-forwarded-for值1.1.1.1(用户欺骗行为),一级代理nginx收到请求后,然后拼接nginx的访问者的ip,那么业务系统会收到x-forwarded-for为1.1.1.1, nginx的ip。这个时候如果代码还是取第一个ip,这个时候就会被用户欺骗,记录1.1.1.1到数据库,这显然不是客户的真实ip

原因3

用户使用vpn上网,你获取的是vpn服务的ip

写法三

public static String getUserIp(HttpServletRequest request){
	String ip = "";
	if (request.getHeader("x-forwarded-for") == null) {
		ip = request.getRemoteAddr();
		if(ip.equals("127.0.0.1")){
			InetAddress inet=null;
			try {
				inet = InetAddress.getLocalHost();
			} catch (Exception e) {
				e.printStackTrace();
			}
			ip= inet.getHostAddress();
		}
	}else{
		ip = request.getHeader("x-forwarded-for");
		if(ip.length() > 15 && ip.indexOf(",")>0){
			ip = ip.substring(0,ip.indexOf(","));
		}
	}
	return ip;
}

恭喜你,90%的可能性获取到了真实的客户ip。

但是我们还是将遇到写法2中的问题,需要运维正确的配置nginx代理,且依然会面临用户的欺诈上送假的x-forwarded-for ,用户使用vpn的情况。


接下来看下nginx的建议配置

       location ^~ /{
           autoindex off;
           proxy_set_header   Host $host;
           proxy_set_header X-Real-IP $remote_addr;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_redirect http://$host/ https://$host/;
           proxy_pass      http://XXXX;
           proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504 http_404;
       }

其中 proxy_set_header X-Real-IP $remote_addr;是另一种客户真实ip方案,在一级代理中配置X-Real-IP为客户ip,二级代理或者业务系统透传X-Real-IP,最终业务系统读取http请求头中的X-Real-IP作为客户的真实ip 其中proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;是拼接传递的形式,最终业务系统获取x_forwarded_for的第一个ip 这两种方案中,目前第二个用的比较多,但是第二个方案目前仍然是不能100%获取到用户的真实ip的,因为用户可能上送一个假的x_forwarded_for值占了第一个ip的位置,但是这已经基本满足了需求了


总结一下,我们应该如何准确的获取到客户的真实ip 

一:约定使用 x_forwarded_for来获取客户ip 

1:业务系统优先获取x_forwarded_for头中的第一个ip,获取不到时,使用remote_addr凑合使用(这个时候我们认为用户可能是内网用户绕过了nginx代理直接向业务系统发起了请求) 2:业务系统前置有nginx代理时,代理准确的配置proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; , 配置得当,且业务系统代码按照1中的逻辑获取,则基本可以满足业务需求 

二:约定 使用 X-Real-IP来获取客户ip 

1:开发和运维统一约定,使用 X-Real-IP 这个http请求头,在一级代理中,设置 proxy_set_header X-Real-IP $remote_addr;,二级代理等后面的代理不再修改此http请求头透传给业务系统

保障业务系统准确获取客户的真实ip需要开发和运维配合处理,双方都应做好完整的沟通,开发取的是哪个http头,运维是否配置代理传递该http头,否则就会引发获取失败,获取错误的问题

探索一下,我们应该如何继续提高获取客户真实ip的准确度 在问题2,3,4中,我们始终没有绕开客户发送请求时伪造假的x_forwarded_for的这个问题,那么我们是否可以考虑丢弃客户发送过来的x_forwarded_for,自己来掌控这个http头呢 其实是可以的,我们可以在一级代理中配置proxy_set_header X-Forwarded-For $remote_addr;因为一级代理再往外就是客户的请求了,一级代理收到的请求来源ip应该是客户的真实公网ip,如果存在二级代理,则二级代理配置 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 但是...............这个方案依然是有风险的,如果业务系统在一级代理前,不是接受客户的直接请求,而是存在全站cdn,用户的请求是先发给cdn的,cdn直接返回静态资源,请求一级代理访问动态资源(这个时候其实cdn也充当了代理),如果我们强制proxy_set_header X-Forwarded-For $remote_addr;这个时候我们将记录到的是cdn的ip,所以当存在cdn时,还是乖乖的在nginx代理里设置proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;吧