php获取客户端真实ip及其原理
Table of Contents
php获取客户端IP的方法
一般情况下,php可以用$_SERVER['REMOTE_ADDR']
来获取客户端ip,REMOTE_ADDR的REMOTE就是远程,ADDR就是address,在这里指ip地址,所以REMOTE_ADDR就是远程的IP地址的意思,那对服务器来说,发起请求的客户端地址就是远程ip地址。
但有时候,你会发现这个地址并不是客户端地址,而是一直不变的一个地址,为什么会这样?原因就是使用了反代服务器,所有来自远程客户端的请求,都是请求的反代服务器,反代服务器再去请求真实服务器,如果反代服务器只有一台,则真实提供服务的服务器收到的所有请求都来自反代服务器,那$_SERVER['REMOTE_ADDR']
当然就是反代服务器的ip,当然也就一直不变。
有反代时获取客户端IP
那如果我想要获取远程客户端的真实地址呢?其实还有另一个变量 $_SERVER['HTTP_X_FORWARDED_FOR']
,通过这个变量就可以获取真实的地址,但是如果未使用代理服务器的服务器,就不会有这个变量,所以注意判断一下这个变量是否存在,比如像下边这么写:
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$list = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
$_SERVER['REMOTE_ADDR'] = $list[0];
}
可以看到,代码里有这一句
$list = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
这句的意思是按逗号分割$_SERVER['HTTP_X_FORWARDED_FOR']
的值,这说明$_SERVER['HTTP_X_FORWARDED_FOR']
是一个逗号分隔的字符串。什么?它不是一个ip么?怎么是逗号分隔的字符串?是的,它确实不一定只是一个ip,它有可能是由逗号分隔的多个ip,原因是代理服务器有可能有多个。
比如现在有三台服务器及一个客户端:
客户端 192.168.11 (访问A)
A 192.168.1.1 (反代到B)
B 192.168.1.2 (反代到C)
C 192.168.1.3 (web服务器)
在服务器C里获取$_SERVER['HTTP_X_FORWARDED_FOR']
变量的值,你猜它是什么?它其实是一个这样的字符串192.168.1.11, 192.168.1.1, 192.168.1.2
,也就是说,每一级返代服务器的ip,都会按顺序被记录在$_SERVER['HTTP_X_FORWARDED_FOR']
变量中,用逗号分隔,所以前面的代码里都会把它用逗号分割并获取第一个元素,因为只有第一个才是真正客户端的ip,其它的都是反代服务器的ip,即使只有一个ip(即没有逗号),用explode
分割后也会变成一个数组,数组只有一个元素,就是这个ip(不会因为没有逗号而报错)。
所以,在获取ip的代码入口里,就可以像上边的代码那样做:如果$_SERVER['HTTP_X_FORWARDED_FOR']
存在,统一把$_SERVER['REMOTE_ADDR']
地址转成$_SERVER['HTTP_X_FORWARDED_FOR']
,这样后面直接用$_SERVER['REMOTE_ADDR']
就可以了。
Nginx配置反代IP变量
问题: 按前面所说,在有反代服务器的情况下,为什么会有$_SERVER['HTTP_X_FORWARDED_FOR']
这个变量呢?
答: 其实HTTP_X_FORWARDED_FOR是一个http header,是在nginx配置里面设置的,如下代码中的proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
就是设置这个Header的:
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass https://test.xiebruce.top/;
}
解释一下上面的配置,以上配置是在Nginx反向代理的时候,添加一些请求Header:
- 1、Host包含客户端真实的域名和端口号;
- 2、X-Forwarded-Proto表示客户端真实的协议(http还是https);
- 3、X-Real-IP表示客户端真实的IP;
- 4、X-Forwarded-For这个Header和X-Real-IP类似,但它在多层代理时会包含真实客户端及中间每个代理服务器的IP。
实际上只要你愿意,你可以自定义任何Header,比如:
proxy_set_header Test 'this is a test';
特别注意:proxy_set_header Host $http_host;
设置后,被代理的服务器(即真实提供服务的服务器)的nginx配置里要设置两个域名(server_name),一个用于被代理服务器识别,另一个就是网站本身的域名,比如有两台机,分别为:
- A:a.test.com
- B:b.test.com
A作为反代服务器反代到B服务器,也就是用户请求的是A服务器的a.test.com,但实际提供服务的是b.test.com,那么在A服务器里写反向代理配置:
server_name a.test.com;
……
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://b.test.com/;
}
B服务器配置(只演示server_name要写两个):
server {
server_name b.test.com a.test.com
//...
}
为什么要这样写呢?因为nginx服务器是不看你浏览器上的域名来匹配它的server_name的,它是看Header中的域名来匹配的:
如上图所示,我们用浏览器查看一个网页,实际上是用浏览器发出一个http请求,浏览器地址栏上的地址会被DNS解析成ip,最终指向网站所在的服务器,所以实际上服务器接收到的请求是ip请求,那服务器怎么知道你访问的是哪个vhost呢(即哪个域名)?没错,nginx是通过识别HTTP请求中的HTTP Header中一个叫HOST的属性来判断的,这好像是很正常,浏览器地址栏上的域名就应该跟header里的host一样啊,好像没什么特别的。
但实际上,请求的域名跟Header里的HOST是有可能不一样的。比如,如果这个请求不是由浏览器发起的,而是由nginx发起的呢?即nginx反代服务器向实际提供网站服务的服务器发起HTTP请求(通过proxy_pass请求b.test.com),也是通过DNS找到了该B服务器地址,然后B服务器的nginx会根据请求的HTTP Header中的HOST来找它这里有没有这个vhost(server_name),那HOST的值是什么?HOST是由A服务器的proxy_set_header Host $http_host;
配置项设置的,而该配置项设置的域名,就是A服务器自己的域名(即a.test.com,因为$http_host
就是A服务器的域名)。
所以B服务器就会在自己的服务器里找a.test.com,到了这里就懂了吧,如果B服务器不设置server_name b.test.com a.test.com,那么B服务器根本找不到域名为a.test.com的vhost,所以就会报:No input file specified。这就是为什么B服务器要配置两个域名的原因。
总结:B服务器中的nginx配置的两个server_name,其中的b.test.com是用于承接A服务器nginx配置中的“proxy_pass http://b.test.com/”的,而“a.test.com”是用来承接原始请求中的Host属性的(如果没有a.test.com,会报502 Bad Gateway,当然另一种方法就是不要设置Host属性,就不会有这个问题)。
特别注意:如果你是一台机测试,是不能同时设置两个相同的域名的,它会自动忽略其中一个的,这样会造成502 Bad Gateway。
如果你想在一台机上测试,那就要注释掉A服务器中的proxy_set_header Host $http_host;
设置,否则会报错。
完整可运行的nginx配置
以下为完整的可运行的nginx配置,可以本地同一台机上测试运行,注意proxy_set_header Host $http_host;
是注释的,如果你不注释(也就是设置了Host)的话,那么b.test.com那边必须用注释的那个(就是有两个域名的那个),但这样的话你就不能在同一台机测试,否则会因为同一台机有两个相同的域名而被nginx自动忽略其中一个
# a.test.com
server {
listen 80;
server_name a.test.com;
charset utf-8;
default_type text/html;
access_log /usr/local/var/log/nginx/a.test.com.access.log;
error_log /usr/local/var/log/nginx/a.test.com.error.log;
location / {
#proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://b.test.com/;
}
}
# b.test.com
server {
listen 80;
# server_name b.test.com a.test.com;
server_name b.test.com;
charset utf-8;
default_type text/html;
root /Users/bruce/www/personal/b.test.com;
access_log /usr/local/var/log/nginx/a.test.com.access.log;
error_log /usr/local/var/log/nginx/a.test.com.error.log;
location / {
try_files $uri $uri/ index.php$is_args$args;
}
location ~ [^/].*\.php(/|$){
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi.conf;
}
}
注意,/Users/bruce/www/personal/b.test.com
是一个目录,你需要设置成你本地的路径,不能直接用我的。
另外你需要在“b.test.com”文件夹下创建一个test.php文件,内容如下
<?php
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
echo '通过反向代理访问,$_SERVER[\'HTTP_X_FORWARDED_FOR\']的值为:'.$_SERVER['HTTP_X_FORWARDED_FOR'].'<br>';
}else{
echo '直接访问,$_SERVER[\'HTTP_X_FORWARDED_FOR\']不存在!<br>';
}
还需要在你的hosts文件中添加以下解析
127.0.0.1 a.test.com
127.0.0.1 b.test.com
最后可分别访问以下两个域名
访问:http://a.test.com/test.php
输出:通过反向代理访问,$_SERVER['HTTP_X_FORWARDED_FOR']的值为:127.0.0.1
访问:http://b.test.com/test.php
输出:直接访问,$_SERVER['HTTP_X_FORWARDED_FOR']不存在!