获取客户端真实 IP
Posted sp42a
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了获取客户端真实 IP相关的知识,希望对你有一定的参考价值。
Tomcat + nginx 反向代理获取客户端真实IP、域名、协议、端口
Nginx 反向代理后,Servlet 应用通过 request.getRemoteAddr()
取到的 IP 是 Nginx 的 IP 地址,并非客户端真实 IP,通过 request.getRequestURL()
获取的域名、协议、端口都是 Nginx 访问 Web 应用时的域名、协议、端口,而非客户端浏览器地址栏上的真实域名、协议、端口。
例如在某一台 IP 为 10.4.64.22 的服务器上,Tomcat 端口号为 8080,Nginx 端口号80,Nginx 反向代理 8080 端口:
server
listen 80;
location /
proxy_pass http://127.0.0.1:8080; # 反向代理应用服务器HTTP地址
在另一台机器上用浏览器打 开http://10.4.64.22/test 访问某个 Servlet 应用,获取客户端 IP 和 URL:
System.out.println("RemoteAddr: " + request.getRemoteAddr());
System.out.println("URL: " + request.getRequestURL().toString());
结果是:
RemoteAddr: 127.0.0.1
URL: http://127.0.0.1:8080/test
可以发现,Servlet 程序获取到的客户端 IP 是 Nginx 的 IP 而非浏览器所在机器的 IP,获取到的URL是 Nginx proxy_pass 配置的URL组成的地址,而非浏览器地址栏上的真实地址。如果将 Nginx 用作 https 服务器反向代理后端的 http 服务,那么request.getRequestURL()
获取的 URL 是 http 前缀的而非 https 前缀,无法获取到浏览器地址栏的真实协议。如果此时将 request.getRequestURL()
获取得到的 URL 用作拼接 Redirect 地址,就会出现跳转到错误的地址,这也是 Nginx 反向代理时经常出现的一个问题。
问题产生的原因
Nginx 的反向代理实际上是客户端和真实的应用服务器之间的一个桥梁,客户端(一般是浏览器)访问 Nginx 服务器,Nginx 再去访问 Web 应用服务器。对于Web应用来说,这次 HTTP 请求的客户端是 Nginx 而非真实的客户端浏览器,如果不做特殊处理的话,Web 应用会把 Nginx 当作请求的客户端,获取到的客户端信息就是 Nginx 的一些信息。
解决方案
解决这个问题要从两个方面来解决:
- 由于 Nginx 是代理服务器,所有客户端请求都从 Nginx 转发到 Tomcat,如果Nginx不把客户端真实IP、域名、协议、端口告诉Jetty/Tomcat,那么Jetty/Tomcat应用是永远不会知道这些信息的,所以需要 Nginx 配置一些HTTP Header来将这些信息告诉被代理的Jetty/Tomcat;
- Jetty/Tomcat这一端,不能再傻乎乎的获取直接和它连接的客户端(也就是 Nginx)的信息,而是要从 Nginx 传递过来的 HTTP Header 中获取客户端信息。
Nginx 添加以下配置:
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
解释以下上面的配置,以上配置是在Nginx反向代理的时候,添加一些请求Header。
- Host 包含客户端真实的域名和端口号;
- X-Forwarded-Proto 表示客户端真实的协议(http 还是 https);
- X-Real-IP 表示客户端真实的 IP;
- X-Forwarded-For 这个 Header 和 X-Real-IP 类似,但它在多层代理时会包含真实客户端及中间每个代理服务器的 IP。
再试一下 request.getRemoteAddr()
和 request.getRequestURL()
的输出结果:
RemoteAddr: 127.0.0.1
URL: http://10.4.64.22/test
可以发现 URL 好像已经没问题了,但是 IP 还是本地的IP而非真实客户端 IP。但是如果是用 Nginx 作为 https 服务器反向代理到 http 服务器,会发现浏览器地址栏是 https 前缀但是 request.getRequestURL()
获取到的 URL 还是 http 前缀,也就是仅仅配置 Nginx 还不能彻底解决问题。
Tomcat
如果你在网上搜索“Java如何获取客户端真实 IP”,搜索到的解决方案大多是通过获取 HTTP 请求头 request.getHeader("X-Forwarded-For")
或 request.getHeader("X-Real-IP")
来实现,也就是上面在 Nginx 上配置的 Header,这种方案获取的结果的确是正确的,但是我个人觉得并不优雅。因为既然 Servlet API 提供了 request.getRemoteAddr()
方法获取客户端 IP,那么无论有没有用反向代理对于代码编写者来说应该是透明的。下面介绍一种更加优雅的方式。
如果使用 Tomcat 作为应用服务器,可以通过配置 Tomcat 的 server.xml
文件,在 Host
元素内最后加入:
<Valve className="org.apache.catalina.valves.RemoteIpValve" />
利用X-Forwarded-For伪造客户端IP漏洞成因及防范
问题背景
在 Web 应用开发中,经常会需要获取客户端IP地址。一个典型的例子就是投票系统,为了防止刷票,需要限制每个 IP 地址只能投票一次。
如何获取客户端 IP
在 Java 中,获取客户端 IP 最直接的方式就是使用 request.getRemoteAddr()
。这种方式能获取到连接服务器的客户端 IP,在中间没有代理的情况下,的确是最简单有效的方式。但是目前互联网 We b应用很少会将应用服务器直接对外提供服务,一般都会有一层 Nginx 做反向代理和负载均衡,有的甚至可能有多层代理。在有反向代理的情况下,直接使用 request.getRemoteAddr()
获取到的 IP 地址是 Nginx 所在服务器的IP地址,而不是客户端的IP。
HTTP协议是基于TCP协议的,由于request.getRemoteAddr()获取到的是TCP层直接连接的客户端的IP,对于Web应用服务器来说直接连接它的客户端实际上是Nginx,也就是TCP层是拿不到真实客户端的IP。
为了解决上面的问题,很多HTTP代理会在HTTP协议头中添加 X-Forwarded-For
头,用来追踪请求的来源。X-Forwarded-For
的格式如下:
X-Forwarded-For: client1, proxy1, proxy2
X-Forwarded-For
包含多个IP地址,每个值通过逗号+空格分开,最左边(client1)是最原始客户端的IP地址,中间如果有多层代理,每一层代理会将连接它的客户端IP追加在X-Forwarded-For右边。
下面就是一种常用的获取客户端真实IP的方法,首先从HTTP头中获取X-Forwarded-For,如果X-Forwarded-For头存在就按逗号分隔取最左边第一个IP地址,不存在直接通过request.getRemoteAddr()获取IP地址:
public String getClientIp(HttpServletRequest request)
String xff = request.getHeader("X-Forwarded-For");
if (xff == null)
return request.getRemoteAddr();
else
return xff.contains(",") ? xff.split(",")[0] : xff;
另外,要让Nginx支持X-Forwarded-For头,需要配置:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
$proxy_add_x_forwarded_for
会将和 Nginx 直接连接的客户端IP追加在请求原有 X-Forwarded-For 值的右边。
伪造 X-Forwarded-For
一般的客户端(例如浏览器)发送HTTP请求是没有X-Forwarded-For头的,当请求到达第一个代理服务器时,代理服务器会加上X-Forwarded-For请求头,并将值设为客户端的IP地址(也就是最左边第一个值),后面如果还有多个代理,会依次将IP追加到X-Forwarded-For头最右边,最终请求到达Web应用服务器,应用通过获取X-Forwarded-For头取左边第一个IP即为客户端真实IP。
但是如果客户端在发起请求时,请求头上带上一个伪造的 X-Forwarded-For,由于后续每层代理只会追加而不会覆盖,那么最终到达应用服务器时,获取的左边第一个IP地址将会是客户端伪造的IP。也就是上面的Java代码中getClientIp()方法获取的IP地址很有可能是伪造的IP地址,如果一个投票系统用这种方式做的IP限制,那么很容易会被刷票。
伪造 X-Forwarded-For 头的方法很简单,例如Postman就可以轻松做到:
Postman伪造X-Forwarded-For
当然你也可以写一段刷票程序或者脚本,每次请求时添加 X-Forwarded-For 头并随机生成一个IP来实现刷票的目的。
如何防范
方法一:在直接对外的 Nginx 反向代理服务器上配置:
proxy_set_header X-Forwarded-For $remote_addr;
这里使用 $remote_addr
替代上面的KaTeX parse error: Double subscript at position 12: proxy_add_x_̲forwarded_for。`proxy_add_x_forwarded_for会在原有X-Forwarded-For上追加IP,这就相当于给了伪造 X-Forwarded-For 的机会。而
$remote_addr` 是获取的是直接 TCP 连接的客户端 IP(类似于 Java 中的request.getRemoteAddr()),这个是无法伪造的,即使客户端伪造也会被覆盖掉,而不是追加。
需要注意的是,如果有多层代理,那么只要在直接对外访问的 Nginx 上配置 X-Forwarded-For 为 $remote_addr
,内部层的 Nginx 还是要配置为 $proxy_add_x_forwarded_for
,不然内部层的 Nginx 又会覆盖掉客户端的真实 IP。
方法二:另外一种方法是我在 Tomcat 源码中发现的:[org.apache.catalina.valves.RemoteIpValve](https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/RemoteIpValve.html)
实现思路:遍历 X-Forwarded-Fo r头中的 IP 地址,和上面方法不同的是,不是直接取左边第一个 IP,而是从右向左遍历。遍历时可以根据正则表达式剔除掉内网 IP 和已知的代理服务器本身的 IP(例如192.168开头的),那么拿到的第一个非剔除 IP 就会是一个可信任的客户端 IP。这种方法的巧妙之处在于,即时伪造 X-Forwarded-For
,那么请求到达应用服务器时,伪造的 IP 也会在 X-Forwarded-For
值的左边,从右向左遍历就可以避免取到这些伪造的 IP 地址。这种方式本文就不提供具体实现代码了,有兴趣可以查看 Tomcat 源码。
以上是关于获取客户端真实 IP的主要内容,如果未能解决你的问题,请参考以下文章