php获取客户端真实ip及其原理

php获取客户端真实ip及其原理

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中的域名来匹配的:

Xnip2019-01-04_03-18-15.jpg

如上图所示,我们用浏览器查看一个网页,实际上是用浏览器发出一个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']不存在!
打赏
订阅评论
提醒
guest

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

0 评论
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x