HOWTO · PHP

在 PHP 中获取用户 IP 地址

本教程演示如何在 PHP 中获取用户的 IP 地址

本页内容

要读取与 PHP 请求关联的网络地址,可先查看 $_SERVER['REMOTE_ADDR']。它标识连接到 Web 服务器的对端。如果应用前面有反向代理,对端可能是代理,而不是访客。只有在应用明确配置为信任提供并验证相关信息的代理时,才可使用代理相关的请求标头。

使用 $_SERVER['REMOTE_ADDR'] 读取连接地址

$_SERVER 是 PHP 超全局变量,包含服务器和请求信息。其中的 REMOTE_ADDR 元素保存连接到处理该请求的服务器的对端地址。直接连接时,对端通常是发起连接的客户端。经过负载均衡器或反向代理时,对端通常是代理的地址。PHP 收到的值由 Web 服务器和代理的配置决定,因此不能把它描述为必定属于最终用户的地址。

对于本地请求,对端可能是 IPv6 回环地址 ::1。回环地址指向本机;::1 是 IPv6 回环地址,对应 IPv4 的 127.0.0.1。具体值取决于本地服务器和连接。请参阅这篇回环地址说明。

下面的可运行示例设置了一个具有代表性的服务器值,使查找过程可以独立演示。示例将 display_output 设为 false,因此会执行查找,但不会输出供显示的内容;在应用中,应根据自己的响应或日志需求使用该值。代码和输出示例中的英文标签保持原样。

本节中的代码以及显示的标签或输出仍为英文。

<?php
$_SERVER['REMOTE_ADDR'] = '::1';
$peerAddress = $_SERVER['REMOTE_ADDR'];
$display_output = false;
if ($display_output) {
    echo "The connected peer is: " . $peerAddress . PHP_EOL;
}
?>

对于实际请求,应使用服务器提供的 $_SERVER['REMOTE_ADDR'] 值,而不是自行赋予示例值。PHP 手册的服务器变量参考介绍了可用的 $_SERVER 条目。

仅在信任代理时才解读代理标头

PHP 可能会将请求标头作为 $_SERVER 条目提供,例如 HTTP_X_FORWARDED_FOR 和 HTTP_CLIENT_IP。这些条目存在,并不能证明其中包含访客的地址:客户端可以自行发送标头值,代理也可能保留、追加或替换这些值。尤其是,HTTP_X_FORWARDED_FOR 可能包含由逗号分隔的一串地址,而不只是一个地址。如果不按基础设施中明确记录的信任规则处理,就不要选取第一个值并称其为真实客户端地址。

只有在连接对端属于应用明确信任的代理,并且该代理配置为移除或验证客户端提供的转发标头时,才使用转发值。受信任代理列表必须与实际部署相符;不要把示例地址直接用作生产环境设置。如果对端不受信任,就使用 REMOTE_ADDR。HTTP_CLIENT_IP 也有相同的信任问题,不应把它当作共享互联网连接时的特殊答案。

下面的可运行代码片段演示一种部署方式:受信任的代理会用一个经过验证的地址覆盖 X-Forwarded-For。请将示例代理地址和验证策略替换为基础设施实际规定的值。对于这些具有代表性的值,示例会输出所选的转发地址;它不会解析多代理地址链。代码和输出示例中的英文标签保持原样。

本节中的代码以及显示的标签或输出仍为英文。

<?php
$_SERVER['REMOTE_ADDR'] = '192.0.2.10';
$_SERVER['HTTP_X_FORWARDED_FOR'] = '198.51.100.24';
$trustedProxies = ['192.0.2.10'];
$peerAddress = $_SERVER['REMOTE_ADDR'];

if (in_array($peerAddress, $trustedProxies, true)
    && isset($_SERVER['HTTP_X_FORWARDED_FOR'])
    && filter_var($_SERVER['HTTP_X_FORWARDED_FOR'], FILTER_VALIDATE_IP) !== false) {
    $clientAddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $clientAddress = $peerAddress;
}

echo "Selected address: " . $clientAddress . PHP_EOL;
?>
Selected address: 198.51.100.24

filter_var() 的文档介绍了此处使用的验证函数。验证字符串是否为 IP 地址,并不能证明它来自可信来源;是否信任该值,取决于对端检查和代理配置。如果代理转发的是一串地址,应使用框架,或使用经过仔细审查的代码,并按照部署中明确规定的受信任代理规则逐项处理这串地址。

使用条件表达式实现紧凑的回退逻辑

通常,易读的 if/elseif/else 代码块更容易审查,尤其是其中包含代理信任检查时。条件表达式可以简洁地表达相同的选择逻辑,但不会因此让不受信任的标头变得安全。下面的可运行示例使用受信任代理检查,并采用上文所述的单地址覆盖策略。服务器值仅用于示例;代码会输出所选的转发地址。代码和输出示例中的英文标签保持原样。

本节中的代码以及显示的标签或输出仍为英文。

<?php
$_SERVER['REMOTE_ADDR'] = '192.0.2.10';
$_SERVER['HTTP_X_FORWARDED_FOR'] = '198.51.100.24';
$trustedProxies = ['192.0.2.10'];
$peerAddress = $_SERVER['REMOTE_ADDR'];
$forwardedAddress = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';

$clientAddress = in_array($peerAddress, $trustedProxies, true)
    && filter_var($forwardedAddress, FILTER_VALIDATE_IP) !== false
        ? $forwardedAddress
        : $peerAddress;

echo "Selected address: " . $clientAddress . PHP_EOL;
?>
Selected address: 198.51.100.24

回退顺序是有意安排的:仅当连接对端属于受信任代理列表时,才接受经过验证的转发地址;否则保留连接对端地址。括号可以让嵌套的条件表达式更清晰,但嵌套三元运算符更难维护,也不能代替信任检查。应确保代理的标头处理方式与应用的允许列表一致,避免客户端把自己发送的标头变成受信任输入。