Pages

Thursday, 23 January 2020

解决php无法获取客户端的真实IP的问题

一般情况下,php可以用$_SERVER['REMOTE_ADDR']来获取客户端ip,REMOTE_ADDR的REMOTE就是远程,ADDR就是address,在这里指ip地址,所以REMOTE_ADDR就是远程的IP地址的意思,那对服务器来说,发起请求的客户端地址就是远程ip地址。
但有时候,你会发现这个地址并不是客户端地址,而是一直不变的一个地址,为什么会这样?原因就是使用了反代服务器,所有来自远程客户端的请求,都是请求的反代服务器,反代服务器再去请求真实服务器,如果反代服务器只有一台,则真实提供服务的服务器收到的所有请求都来自反代服务器,那$_SERVER['REMOTE_ADDR']当然就是反代服务器的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];
}
PHP
说明:在代码入口里,如果$_SERVER['HTTP_X_FORWARDED_FOR']存在,统一把$_SERVER['REMOTE_ADDR']地址转成$_SERVER['HTTP_X_FORWARDED_FOR'],这样后面直接用$_SERVER['REMOTE_ADDR']就可以了。
在有反代服务器的情况下,为什么会有$_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.mydomain.com/;
}
PHP
解释一下上面的配置,以上配置是在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';
nginx
特别注意:proxy_set_header Host $http_host;设置后,被代理服务器要设置两个域名,一个用于dns解析,另一个与代理服务器相同,比如有两台机,分别为:
A:a.test.com
B:b.test.com
A作为反代服务器反代到B服务器,也就是用户请求的是A服务器的a.test.com,但实际提供服务的是b.test.com,那么在A服务器里写反向代理配置:
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/;
}
PHP
B服务器配置(只演示server_name要写两个):
server {
    server_name b.test.com a.test.com
    //...
}
PHP
为什么要这样写呢?因为nginx服务器是不看你浏览器上的域名来找vhost的,它是看Header中的域名来找vhost的.
我们用浏览器查看一个网页,实际上是用浏览器发出一个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,那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服务器的a.test.com是不用添加DNS解析的,因为DNS已经解析到A服务器的ip上了,就算要解析到B,也是不可能的,而且B服务器上的a.test.com域名,主要是用来被识别vhost用的,因为请求已经通过b.test.com到达该服务器了.