Laravel自带的获取request()->getClientIp();方法,获取的有可能是nginx反向代理的IP,如果我们想获取真实IP,需要运维给我们返回一个真实IP的header头,但是被运维给拒了。
让运维多加个返回,如REAL-IP,这样子,我们就不需要做判断,直接获取?
今天问了运维,告知不用加REAL-IP字段,直接从HTTP_X_FORWARDED_FOR中取第一个值就行,那个值就是$remote_addr。
借助HTTP_X_FORWARDED_FOR获取
经过多个代理服务器时,这个值类似如下:127.0.0.1, 127.0.0.2, 127.0.0.3。所以下面的方法里我做了分割,并取第一个。
借助HTTP_CLIENT_IP和REMOTE_ADDR获取
这两个值都代表了客户端IP,但是有可能是代理IP,所以优先级靠后
如果我们确定要在nginx加自定义header返回时就要注意:nginx反向代理proxy_set_header自定义header头无效的问题。关于这个,我在另一篇文章详述。
PHP获取真实IP方法
- 因为HTTP_X_FORWARDED_FOR是可以伪造的,这里要对过滤后的数组进行array_reverse,因为这样获得的IP才是刨除伪造header头的真实客户端IP。例如:常见的在curl里伪造请求header:
$header = array( 'CLIENT-IP:127.0.0.1', 'X-FORWARDED-FOR:127.0.0.2,127.0.0.3' );
假如用户的真实IP是127.0.0.4, 那么我们获取到的HTTP_X_FORWARDED_FOR就是:
127.0.0.2,127.0.0.3,127.0.0.4
- 如果我们使用了代理,这里还要过滤代理IP, 避免reverse后打到了我们自己的代理IP
3.注意:我这里没做上述的处理,是因为运维在入口处禁止了伪造请求头,HTTP_X_FORWARDED_FOR是可信的,不能代表所有业务场景
/**
* 获取ip
* @return mixed
*/
public function getIp(){
$client_ip='';
if (isset($_SERVER))
{
if (isset($_SERVER["HTTP_X_FORWARDED_FOR"]))
{
//优先使用 HTTP_X_FORWARDED_FOR,此值是一个逗号分割的多个IP
//注意:我这里没做处理,是因为运维在入口处禁止了伪造请求头,HTTP_X_FORWARDED_FOR是可信的,不能代表所有业务场景
//todo 没有禁止伪造请求头下的特殊处理
$ipStr = $_SERVER["HTTP_X_FORWARDED_FOR"];
$ipArr = explode(',',$ipStr);
$client_ip = isset($ipArr[0])?$ipArr[0]:'';
}
else if (isset($_SERVER["HTTP_CLIENT_IP"]))
{
$client_ip = $_SERVER["HTTP_CLIENT_IP"];
}
else
{
$client_ip = $_SERVER["REMOTE_ADDR"];
}
}
//过滤无效IP
if(filter_var($client_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false || filter_var($client_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false){
return $client_ip;
}else{
return $_SERVER["REMOTE_ADDR"];
}
}
Laravel获取真实IP
Laravel社区的Summer3年前有说到,可以使用 setTrustedHeaderName() 做设置。现在这个方法已经被setTrustedProxies代替了。
- the
Request::setTrustedHeaderName()
andRequest::getTrustedHeaderName()
methods have been removed
先看下新的方法作了什么
/*方法接受两个参数.
array proxies,接收我们信任的代理IP数组,这些IP在获取时会被过滤。
int trustedHeaderSet获取信任的HeaderSet
这里是兼容了一些无法返回HEADER_X_FORWARDED_FOR的服务器,
例如AWS,或者符合RFC 7239标准。*/
public static function setTrustedProxies(array $proxies, int $trustedHeaderSet)
{
//设置trustedProxies为处理过的代理,字符串REMOTE_ADDR做特殊处理
self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) {
//将所传代理放置到信任代理中
if ('REMOTE_ADDR' !== $proxy) {
$proxies[] = $proxy;
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
//如果所传参数是字符串“REMOTE_ADDR”,则将当前的REMOTE_ADDR添加到信任代理中
$proxies[] = $_SERVER['REMOTE_ADDR'];
}
return $proxies;
}, []);
self::$trustedHeaderSet = $trustedHeaderSet;
}
可以看到,我们调用这个方法的参数可以写自己信任的IP,或者传字符串REMOTE_ADDR,如果传了REMOTE_ADDR,则将当前的$_SERVER[REMOTE_ADDR]添加到信任代理中
$param = ['REMOTE_ADDR','127.0.0.1'];
request()->setTrustedProxies($param);
然后我们看下当我们执行getClientIp时走了哪些步
public function getClientIp()
{
//从IP池取IP,并返回0下标的IP返回
$ipAddresses = $this->getClientIps();
return $ipAddresses[0];
}
public function getClientIps()
{
//从REMOTE_ADDR取IP
$ip = $this->server->get('REMOTE_ADDR');
//判断是否来自信任代理
if (!$this->isFromTrustedProxy()) {
//如果不在设置的信任代理中,那么直接返回REMOTE_ADDR
return [$ip];
}
//这里才会从HEADER_X_FORWARDED_FOR中获取真实IP
return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
}
//注意看这里$trustedProxies就是我们设置信任IP的那个方法存储的属性,这里会判断当前的REMOTE_ADDR是否在我们信任的代理池中,如果没设置或者没在,就返回false
public function isFromTrustedProxy()
{
return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
}
大致意思我来总结下:
如果不调用setTrustedProxies方法,或者REMOTE_ADDR的IP不在setTrustedProxies方法设置的IP代理池中,那么getClientIp返回的就是REMOTE_ADDR的IP。
怎么处理
这个场景很熟悉了,那么我们找运维要一下我们所有的代理IP,并通过setTrustedProxies设置下就行了。
实际应用
嗯,表面上如此,当运维发给我们一个IP段呢?淦!
犹记得,当年微信公众号开发,信任IP地址不支持填IP段,我是自己写了个脚本生成的。
峰回路转
setTrustedProxies方法的第一个参数有这个判断:
如果所传参数是字符串“REMOTE_ADDR”,则将当前的REMOTE_ADDR添加到信任代理中
**setTrustedProxies**方法的第二个参数:获取信任的HeaderSet
那么,我们就可以如下这样设置,默认不信任REMOTE_ADDR的IP,如果没有获取到真实IP,才使用这个REMOTE_ADDR。
laravel一般情况下获取真实IP的方法
/***
不同HEADER的int值
const HEADER_FORWARDED = 0b00001; // When using RFC 7239
const HEADER_X_FORWARDED_FOR = 0b00010;
const HEADER_X_FORWARDED_HOST = 0b00100;
const HEADER_X_FORWARDED_PROTO = 0b01000;
const HEADER_X_FORWARDED_PORT = 0b10000;
const HEADER_X_FORWARDED_ALL = 0b11110; // All "X-Forwarded-*" headers
const HEADER_X_FORWARDED_AWS_ELB = 0b11010; // AWS ELB doesn't send X-Forwarded-Host
***/
public function getIp(){
request()->setTrustedProxies(['REMOTE_ADDR'],0b00010);
$ip = request()->getClientIp();
}
如上,我使用了HEADER_X_FORWARDED_FOR作为我信任的真实IP来源,取不到才使用REMOTE_ADDR。
注意,我这里只使用了REMOTE_ADDR,如果有别的代理IP可以往数组里加。
根据使用场景做了修改
运维在入口做了限制,我们不用再关心伪造header头的问题了,所以,这里还是由左开始取IP。(原方法会执行array_reverse)
public function getIp(){
request()->setTrustedProxies(['REMOTE_ADDR'],0b00010);
$ip = array_reverse(request()->getClientIps())[0];
return $ip;
}
结语
一个简单的方法,后面是无数的兼容逻辑。