出于多种原因(记录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
这三种方式各有利弊,一般混合使用,但仍有弊端
看一段网上的代码,综合了这三种,处理逻辑:
- 取
HTTP_X_FORWARDED_FOR
参数第一个合法ip: 可伪造,兼容存在代理的情形 - 取
HTTP_CLIENT_IP
参数:非标准协议不一定有此参数,可伪造 - 取
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
37protected 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的获取方式分析:
$request->getClientIp()
为调用的vendor/symfony/http-foundation/Request.php
中的getClientIp()
函数:
可见此函数为直接调用getClientIps()
函数获取用户所有ip列表,将第0个返回作为用户真实ip1
2
3
4
5
6public function getClientIp()
{
$ipAddresses = $this->getClientIps();
return $ipAddresses[0];
}vendor/symfony/http-foundation/Request.php
的getClientIps()
函数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
38public 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