序
最近有一个项目(调用第三方接口)需要添加验证白名单功能,周五和老大在探讨的时候,我们说到了获取ip ,我当时是这么说的
我:
“我让header 传递ip过来 ”
老大:
“直接request 直接获取ip不就行了么 ”
老大意思应该是直接调用如下这类代码搞定
package com.example.winterholity.util;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class IpUtil {
public static String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
if (ipAddress.equals("127.0.0.1")) {
// 根据网卡取本机配置的IP
InetAddress inet = null;
try {
inet = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
e.printStackTrace();
}
ipAddress = inet.getHostAddress();
}
}
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
// = 15
if (ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
}
} catch (Exception e) {
ipAddress="";
}
// ipAddress = this.getRequest().getRemoteAddr();
return ipAddress;
}
}
但是我感觉不一定
问题
这个方法就一定获取的是真实的IP么?
先搭建一个环境再说
简单测试 ip获取成功
但是在互联网 请求 nginx 的接口,业务系统获取的是 nginx的ip,这就不对了,应该是返回互联网的IP 才对
如果request.getRemoteAddr() 就可以获取真实ip,那就不会有x-forwarded-for 等属性的事了
request.getRemoteAddr() 和 request.getRemoteHost() 区别
System.out.println("request.getRemoteAddr(): " + request.getRemoteAddr());
System.out.println("request.getRemoteHost(): " + request.getRemoteHost());
前一个是获得客户端的ip地址
后一个是获得客户端的主机名
request.getRemoteAddr() 里面的ip 是怎么获取的
public String getRemoteAddr() {
if (this.remoteAddr == null) {
this.coyoteRequest.action(ActionCode.REQ_HOST_ADDR_ATTRIBUTE, this.coyoteRequest);
this.remoteAddr = this.coyoteRequest.remoteAddr().toString();
}
return this.remoteAddr;
}
不一定获取的是真实的IP,可能获取的是 nginx等中间件的ip,甚至伪造的也是可能的
因为负载均衡 可能丢弃真实ip,有什么办法进行
应用层
在这一层,Web 协议的制定者们想到了一个巧妙的办法:既然 HTTP 协议比较灵活,那就可以设计一个新的 header,用来传递真实源 IP,它就是 X-Forwarded-For。这个标准最初是 Squid 的开发工程师提出的,很快受到了业界的支持,各种 web 服务器都早已支持了这个 header。
他是通过X-Forwarded-For 直接传递进去
不过,X-Forwarded-For 这个标准,虽然用一种相对低的成本解决了“服务器不能获取真实源 IP”的问题,但它本身还是有一些不足的
源 IP 信息的伪造问题
这也是它最大的问题,因为这个头部本身没有任何安全保障机制,攻击者完全可以任意构造 X-Forwarded-For 信息来欺骗服务端。比如,如果攻击者知道服务端对某个 IP 段来的请求进行特殊处理(比如会提供更大力度的优惠券),那么攻击者就可以在发送请求时候,构造一个 X-Forwarded-For 头部,它的值就是这个段内的某个 IP。当服务端收到请求时,认为 X-Forwarded-For 里排在最左边的 IP 是真实 IP,而事实上这个是伪造出来的,所以可想而知,这个请求就可以获取它原本不应该得到的特权了。
重复的 X-Forwarded-For 头部
HTTP 协议本身并不严格要求 header 是唯一的,所以有些情况下,HTTP 请求可能会携带两个或者更多的 X-Forwarded-For 头部。
造成这个现象的原因是,某些代理或者 LB 并不是严格按照协议规定的,把 IP 附加到已有的 X-Forwarded-For 头部,而是自己另起一个 X-Forwarded-For 头部,那么这样就导致了重复的 X-Forwarded-For。
对于服务端来说,在收到这种请求的时候,可能会导致信息识别上的错乱。比如某些服务端的逻辑是读取第一个 X-Forwarded-For,而另外一些服务端程序可能是读取最后一个,并无定法
不能解决 HTTP 和邮件协议以外的真实源 IP 获取的需求
X-Forwarded-For 解决了 HTTP 的透传真实源 IP 的需求,但是事实上,很多应用并不是基于 HTTP 协议工作的,比如数据库、FTP、syslog 等等,这些场景也需要“获取真实源 IP”这个功能。但是前面说的 X-Forwarded-For,只能为 HTTP/ 邮件协议所用,那其他这么多协议和应用难道就成了没妈的孩子,永远不能获取到真实源 IP 了吗?
这时候,传输层的方法就上场了。
传输层方法
TOA 和 TCP Options
TOA 全称是 TCP Option Address,它是利用 TCP Options 的字段来承载真实源 IP 信息,这个是目前比较常见的第四层方案。不过,这并非是 TCP 标准所支持的,所以需要通信双方都进行改造。也就是:
对于发送方来说,需要有能力把真实源 IP 插入到 TCP Options 里面。
对于接收方来说,需要有能力把 TCP Options 里面的 IP 地址读取出来。
可见,TCP Options 是可变长的,最长为 40 字节(第一列的偏移量 20 到 60 字节之差)。每个 Option 项由三部分组成:
扩展 SYN 报文的 TCP Options,让它携带真实源 IP 信息。这个需要对中间的 LB 和后端服务器都进行小幅的配置改造。
Proxy Protocol
这个方案是 HAProxy(另外一个广泛应用的反向代理软件)工程师提出的。它的实现原理是这样的:
这是一个逐步被各种反向代理和 HTTP Server 软件接纳的方案,可以在不改动代码或者内核配置的情况下,只修改反向代理和 HTTP Server 软件的配置就能做到。
为什么会有 x-forwarded-for
我们第三方调用不是http 就是webservice,其他的很少(其中http是大多数),针对这种情况,我们都会在nginx配置如下
如果发送请求设置X-Forwarded-For=xxx,然后nginx 设置
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:9009/;
proxy_redirect off;
}
后台 获取的ip为 xxx的IP,实际获取的ip为xxx
上面这个配置有问题么?
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
这样配置之后,安全性确实提高了,但是也导致请求到达 Nginx 之前的所有代理信息都被抹掉,无法为真正使用代理的用户提供更好的服务。还是应该弄明白这中间的原理,具体场景具体分析。
应该用
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:9009/;
proxy_redirect off;
}
思考题
我有一个场景也是用了本文章的内容,您看看怎么实现
背景
我们对接了很多的第三方平台接口,需要部署我们的agent,但是第三方接口经常出问题,但是用户看到有问题了,肯定找我们,或者对我们的影响不好
第三方接口都是http请求,但是其中 怎么转发的不清楚
然后我们就要找原因,找到是2,3 中间的问题,但是这两方都不认为是自己的问题,让我们找证据,他们的网络环境,对我们来说是黑盒,这种情况,你怎么定位问题出现在哪
需求
分析
其实如果LB 都是我们自己维护 ,当然怎么搞都可以实现,但是往往调用第三方接口,LB 不一定怎么实现的,有的 直接header 给你过滤掉都是有可能的,这就给我们增加了难度