随便说点
开工啦,新年上班第一天,本来这篇博客是年前放假的头一天就该写完的,但是,由于放假心情比较激动,按耐不住,还是拖到现在才写。
背景
一个偶然的时机,在查看系统管理后台的操作日志和登录日志的时候,发现ip地址一列有点不对劲,全是服务器的ip地址,但是那个地方应该是用来记录用户的真实ip的,可是我去tail -f 查看nginx的日志的时候 明明记录的就是客户端的真实ip呀,觉得很奇怪,所以决定看看到底是怎么回事。
我们公司内网的网端都是172.16.100.xx,下图中可以看到,在未修改nginx配置前 ip都是服务器的ip,其他正常的都是在修改nginx配置后的真实用户ip。
起初我以为是java端获取用户ip的地方除了问题,于是去找对应的代码,这里说一下,我们是用ruoyi的脚手架去开发的后台管理系统,有兴趣的可以去了解一下 RuoYi
经过一番定位,最终看到获取ip的逻辑代码是IpUtils工具类中的getIpAddr方法
public static String getIpAddr(HttpServletRequest request)
{
if (request == null)
{
return null;
}
String ip = null;
// X-Forwarded-For:Squid 服务代理
String ipAddresses = request.getHeader("X-Forwarded-For");
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses))
{
// Proxy-Client-IP:apache 服务代理
ipAddresses = request.getHeader("Proxy-Client-IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses))
{
// WL-Proxy-Client-IP:weblogic 服务代理
ipAddresses = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses))
{
// HTTP_CLIENT_IP:有些代理服务器
ipAddresses = request.getHeader("HTTP_CLIENT_IP");
}
if (ipAddresses == null || ipAddresses.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses))
{
// X-Real-IP:nginx服务代理
ipAddresses = request.getHeader("X-Real-IP");
}
// 有些网络通过多层代理,那么获取到的ip就会有多个,一般都是通过逗号(,)分割开来,并且第一个ip为客户端的真实IP
if (ipAddresses != null && ipAddresses.length() != 0)
{
ip = ipAddresses.split(",")[0];
}
// 还是不能获取到,最后再通过request.getRemoteAddr();获取
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ipAddresses))
{
ip = request.getRemoteAddr();
}
return ip.equals("0:0:0:0:0:0:0:1") ? "127.0.0.1" : ip;
}
这里有几个ip,分别是x-forward-for、proxy-client-ip、WL-Proxy-Client-IP、HTTP_CLIENT_IP、X-Real-IP、remote_addr
我在调试过程中有把这几个ip都记录到日志中,但是后面看日志 发现这个地方获取的ip确实是服务器的ip,也就是172.16.100.37,于是想到那就应该是nginx那个地方的配置有问题了,于是去看nginx的配置,下面的红框标注的是我修改过后的配置,之前没有,也就是说nginx并没有把x-forward-for这几个ip参数放到http请求头中,导致在后端代码那里获取不到,这里添加的参数 看具体应用场景了,如果你只是想要把ip放到请求头里面的话,加一个x-forwarded-for就行了,我这里的应用场景就是想看一下用户的ip,所以 nginx是像下面那样配置的。
概念
上面那几个ip中 有两个ip的概念容我抄袭一下网上的概念,一个是x-forwarded-for,一个是remote_addr
X-Forwarded-For 是一个 HTTP 扩展头部。HTTP/1.1(RFC 2616)协议并没有对它的定义,它最开始是由 Squid 这个缓存代理软件引入,用来表示 HTTP 请求端真实 IP。如今它已经成为事实上的标准,被各大 HTTP 代理、负载均衡等转发服务广泛使用,并被写入 RFC 7239(Forwarded HTTP Extension)标准之中
remote_addr代表客户端的IP,但它的值不是由客户端提供的,而是服务端根据客户端的ip指定的,当你的浏览器访问某个网站时,假设中间没有任何代理,那么网站的web服务器(Nginx,Apache等)就会把remote_addr设为你的机器IP,如果你用了某个代理,那么你的浏览器会先访问这个代理,然后再由这个代理转发到网站,这样web服务器就会把remote_addr设为这台代理机器的IP。
这样说是不是不太容易理解,那我咀嚼一下
如果一个 HTTP 请求到达服务器之前,每经过一级代理,都会把该代理ip追加在请求头的x-forwarded-for中,但是第一个是用户的ip,然后才是代理1-n的ip。
而remote_addr 简单来说就可以理解为直接和服务端建立tcp连接的ip, 最后直接和服务端建立tcp连接的那个东西的ip 就是remote_addr。
实验
空说无凭,咱们得做实验来证明我说的是对的,环境是这样的
- 本地pc( 10.10.31.22 ) – 运行java服务
- 本地pc(10.10.31.22) apifox – 向服务端(也就是向自己)发起请求
- 本地搭建虚拟机(10.10.30.19) – 上面运行了一个nginx
- 虚拟交换机(172.17.202.33) – 负责nat给虚拟机提供网络访问
虚拟机的配置是下面这样的
测试接口的代码 其实也很简单,就是在后端获取请求头中的几个ip然后放到map后返回
最终apifox的请求结果如下
这里可以看到 remote_addr是虚拟交换机的ip,因为最终数据包会通过虚拟交换机进行转发,也就避免不了需要和服务端建立连接; 而x-forwarded-for的ip和之前解释的一致,因为这里有一个nginx代理,所以在用户端ip的后面 是 客户端ip,nginx代理ip。
整个的请求过程大概是下面的这样:
环境搭建
如果有疑虑,可以自己动手实验,体验更深刻
win10安装配置hyper-v
安装配置nginx
其中编译编译nginx过程中可能遇到的问题
c compiler cc is not found
the HTTP rewrite module requires the PCRE library
the HTTP gzip module requires the zlib library