最近有个需求,需要对管理员登录使用IP白名单的方式来限制登录。在使用了TP6框架的系统中,可以很方便的使用request()->ip()
来获取客户端IP。
但是当我测试时候发现,IP获取是有问题的,实际上获取的是本地地址127.0.0.1
和::1
这两个,经过仔细排查发现问题如下:
- 当使用nginx做了反向代理的时候,虽然正确设置了X-Real-IP和X-Forwarded-For等代理头,但是仍然无法获取到真实IP
- 观察TP6框架中的Request类中的ip方法,发现代码为
/**
* 获取客户端IP地址
* @access public
* @return string
*/
public function ip(): string
{
if (!empty(this->realIP)) {
returnthis->realIP;
}
this->realIP =this->server('REMOTE_ADDR', '');
// 如果指定了前端代理服务器IP以及其会发送的IP头
// 则尝试获取前端代理服务器发送过来的真实IP
proxyIp =this->proxyServerIp;
proxyIpHeader =this->proxyServerIpHeader;
if (count(proxyIp)>0 && count(proxyIpHeader) > 0) {
// 从指定的HTTP头中依次尝试获取IP地址
// 直到获取到一个合法的IP地址
foreach (proxyIpHeader asheader) {
tempIP =this->server(header);
if (empty(tempIP)) {
continue;
}
tempIP = trim(explode(',',tempIP)[0]);
if (!this->isValidIP(tempIP)) {
tempIP = null;
} else {
break;
}
}
// tempIP不为空,说明获取到了一个IP地址
// 这时我们检查 REMOTE_ADDR 是不是指定的前端代理服务器之一
// 如果是的话说明该 IP头 是由前端代理服务器设置的
// 否则则是伪装的
if (!empty(tempIP)) {
realIPBin =this->ip2bin(this->realIP);
foreach (proxyIp as ip) {serverIPElements = explode('/', ip);serverIP = serverIPElements[0];serverIPPrefix = serverIPElements[1] ?? 128;serverIPBin = this->ip2bin(serverIP);
// IP类型不符
if (strlen(realIPBin) !== strlen(serverIPBin)) {
continue;
}
if (strncmp(realIPBin,serverIPBin, (int) serverIPPrefix) === 0) {this->realIP = tempIP;
break;
}
}
}
}
if (!this->isValidIP(this->realIP)) {this->realIP = '0.0.0.0';
}
return $this->realIP;
}
- 观察代码发现,先通过
REMOTE_ADDR
获取了真实IP,但是此时因为通过nginx做了反代,实际上取到的值是nginx代理服务器的ip(因为我是本机反代,所以取到了本机地址) - 其次通过判断proxyServerIp和proxyServerIpHeader来进行有效性验证,这里比较关键的是proxyServerIp这个全局变量,通过查询代码可知,该变量全局未被复制,并且在TP框架其他地方也未进行赋值。但是他却在判断逻辑中充当重要依据,“如果获取到的真实IP是proxyServerIp存储的其中一个IP,则认为他是一个可信的反代服务器(因为其他方式获取到的信息都是可以被伪造的),那么此时可以获取其他字段中携带的客户端IP信息,否则就以REMOTE_ADDR获取到的ip作为真实ip”
- 根据以上结果可以分析出,只需要对proxyServerIp这个变量进行赋值即可。那么此时可以在TP6框架中给用户自定义的Request类中,重新对该变量赋值即可,如下:
class Request extends think\Request
{
protected $proxyServerIp = ['127.0.0.1','::1'];
}
至此,即可获取到有效客户端IP了。当然,你也可以进一步改造,使其可以通过配置文件进行读取。
文章评论