如何正确获取请求方的ip地址(How to get client ip correctly)

出于多种原因(记录client原始ip、定位、反作弊等), 都需要获取请求发起方的原始ip信息。
这里探讨两种方式:1. PHP纯手动获取,2. Laravel提供的getClientIp()函数

通过$_SERVER()或getenv()方式

方式1:通过x-forwarded-for头部
PHP中通过$_SERVER('HTTP_X_FORWARDED_FOR')获取http request请求头部的x-forwarded-for参数
以下为$_SERVER所有参数实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"USER": "work",
"HOME": "/home/work",
"HTTP_ACCEPT_LANGUAGE": "zh-CN,zh;q=0.8,en;q=0.6,zh-TW;q=0.4",
"HTTP_ACCEPT_ENCODING": "gzip, deflate, sdch",
"HTTP_DNT": "1",
"HTTP_ACCEPT": "*/*",
"HTTP_POSTMAN_TOKEN": "b220c1e0-e90b-0955-82ee-eb2ed8784047",
"HTTP_USER_AGENT": "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.71 Safari/537.36",
"HTTP_CACHE_CONTROL": "no-cache",
"HTTP_CONNECTION": "keep-alive",
"HTTP_HOST": "172.16.0.182:8055",
"REDIRECT_STATUS": "200",
"SERVER_NAME": "",
"SERVER_PORT": "8055",
"SERVER_ADDR": "172.16.0.182",
"REMOTE_PORT": "54175",
"REMOTE_ADDR": "172.16.254.2",
"SERVER_SOFTWARE": "nginx/1.9.15",
"GATEWAY_INTERFACE": "CGI/1.1",
"REQUEST_SCHEME": "http",
"SERVER_PROTOCOL": "HTTP/1.1",
"DOCUMENT_ROOT": "/home/work/www/test/public",
"DOCUMENT_URI": "/index.php",
"REQUEST_URI": "/test_url/hello",
"SCRIPT_NAME": "/index.php",
"CONTENT_LENGTH": "",
"CONTENT_TYPE": "",
"REQUEST_METHOD": "GET",
"QUERY_STRING": "name=five",
"SCRIPT_FILENAME": "/home/work/www/test/public/index.php",
"FCGI_ROLE": "RESPONDER",
"PHP_SELF": "/index.php",
"REQUEST_TIME_FLOAT": 1479034455.88,
"REQUEST_TIME": 1479034455
}

弊端:

  • 不经代理时,$_SERVER['HTTP_X_FORWARDED_FOR']参数未定义
  • http request时增加x-forwarded-for参数可伪造用户ip,且该参数可任意输入内容,需进行ip过滤,防注入

该方式下,如存在代理转发,则$_SERVER['HTTP_X_FORWARDED_FOR']中依次存[clientip, proxyip1, proxyip2]
从前往后,第一个合法ip作为用户真实ip

方式2:取$_SERVER['HTTP_CLIENT_IP']$_SERVER['REMOTE_ADDR']
方式1中,当不经代理转发时,是无法拿到用户ip的;因此下面两种方式可以尝试:

  • $_SERVER['HTTP_CLIENT_IP']能获取到http request header中的client-ip头部
    但此头部非标准规范,并不一定存在,且可随意伪造

  • $_SERVER['REMOTE_ADDR']能获取发起tcp请求的原始ip地址
    在不存在代理时,是用户准确ip;存在代理时,该值为最后一个代理ip

这三种方式各有利弊,一般混合使用,但仍有弊端

看一段网上的代码,综合了这三种,处理逻辑:

  1. HTTP_X_FORWARDED_FOR参数第一个合法ip: 可伪造,兼容存在代理的情形
  2. HTTP_CLIENT_IP参数:非标准协议不一定有此参数,可伪造
  3. REMOTE_ADDR参数: 存在代理时获取到的时代理ip,不可伪造
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    protected function getRealIp()
    {
    $realip = '';
    if (isset($_SERVER)) {
    if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
    /* 取X-Forwarded-For中第一个非unknown的有效IP字符串 */
    foreach ($arr as $ip) {
    $ip = trim($ip);
    if ($ip != 'unknown') {
    $realip = $ip;
    break;
    }
    }
    } elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
    $realip = $_SERVER['HTTP_CLIENT_IP'];
    } else {
    if (isset($_SERVER['REMOTE_ADDR'])) {
    $realip = $_SERVER['REMOTE_ADDR'];
    } else {
    $realip = '0.0.0.0';
    }
    }
    } else {
    if (getenv('HTTP_X_FORWARDED_FOR')) {
    $realip = getenv('HTTP_X_FORWARDED_FOR');
    } elseif (getenv('HTTP_CLIENT_IP')) {
    $realip = getenv('HTTP_CLIENT_IP');
    } else {
    $realip = getenv('REMOTE_ADDR');
    }
    }

    preg_match("/[\d\.]{7,15}/", $realip, $onlineip);
    $realip = !empty($onlineip[0]) ? $onlineip[0] : '0.0.0.0';
    return $realip;
    }

上面说的时原生PHP的方式手动取用户ip,下面说说Laravel中支持的getClientIp()函数

通过Laravel的getClientIp()方式

在Laravel(5.2)的Controller或Middleware中,通过$request->getClientIp()可以直接获取用户ip
下面Laravel的获取方式分析:

  1. $request->getClientIp()为调用的vendor/symfony/http-foundation/Request.php中的getClientIp()函数:
    可见此函数为直接调用getClientIps()函数获取用户所有ip列表,将第0个返回作为用户真实ip

    1
    2
    3
    4
    5
    6
    public function getClientIp()
    {
    $ipAddresses = $this->getClientIps();

    return $ipAddresses[0];
    }
  2. vendor/symfony/http-foundation/Request.phpgetClientIps()函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public function getClientIps()
    {
    $clientIps = array();
    $ip = $this->server->get('REMOTE_ADDR');

    if (!$this->isFromTrustedProxy()) {
    return array($ip);
    }

    $hasTrustedForwardedHeader = self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED]);
    $hasTrustedClientIpHeader = self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP]);

    if ($hasTrustedForwardedHeader) {
    $forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
    preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches);
    $forwardedClientIps = $matches[3];

    $forwardedClientIps = $this->normalizeAndFilterClientIps($forwardedClientIps, $ip);
    $clientIps = $forwardedClientIps;
    }

    if ($hasTrustedClientIpHeader) {
    $xForwardedForClientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));

    $xForwardedForClientIps = $this->normalizeAndFilterClientIps($xForwardedForClientIps, $ip);
    $clientIps = $xForwardedForClientIps;
    }

    if ($hasTrustedForwardedHeader && $hasTrustedClientIpHeader && $forwardedClientIps !== $xForwardedForClientIps) {
    throw new ConflictingHeadersException('The request has both a trusted Forwarded header and a trusted Client IP header, conflicting with each other with regards to the originating IP addresses of the request. This is the result of a misconfiguration. You should either configure your proxy only to send one of these headers, or configure Symfony to distrust one of them.');
    }

    if (!$hasTrustedForwardedHeader && !$hasTrustedClientIpHeader) {
    return $this->normalizeAndFilterClientIps(array(), $ip);
    }

    return $clientIps;
    }

函数中常量较多,下面将整个逻辑记录如下:

1. $ip = $this->server->get('REMOTE_ADDR');获取REMOTE_ADDR地址

2. 判断REMOTE_ADDR是否在信任代理列表中,信任列表为空或不在信任列表,则返回return array($ip);

注意:这里的逻辑是信任列表为空或REMOTE_ADDR不在信任列表,则返回此REMOTE_ADDR地址。
也就是说默认未配置信任列表的情况下,Laravel取得用户ip其实是REMOTE_ADDR
简言之:使用代理且未设置信任列表时,getClientIp()获取到的是最后一跳连接服务器的ip(也就是代理的ip)

3. 当信任列表非空且REMOTE_ADDR在信任代理列表时,才会进行后续的操作:

3.1 取header中的forwarded参数值
3.2 取header中的x-forwarded-for参数值列表
注意:Laravel的这部分代码略有混淆,常量的定义和header不一致容易导致误解(特别是trustedHeaders信任头部数组)

1
2
`HEADER_FORWARDED`常量对应header参数`forwarded`
`HEADER_CLIENT_IP`常量对应header参数`x-forwarded-for`

3.3 将x-forwarded-for参数值逐一进行如下操作:正则匹配合法ip,检查是否在信任代理列表(在列表则删除),将最终剩余的ip放入列表,倒序列表,返回列表
至此:得到x-forwarded-for中从前到后,且不在信任列表中的ip参数项。
$request->getClientIp()取第一项作为用户真实ip

PS: 貌似这里仍然无法避免用户通过修改x-forwarded-for参数来伪造用户ip

总结

  • 手动获取处理的方式要考虑各种网络环境
  • Laravel的方式依托信任代理列表的思想,将倒数第一个非信任列表代理地址作为用户ip
  • 代理模式下,依然无法做到完全的防止用户伪造ip

参考