在Laravel中一般使用Illuminate\Http\Request::ip()
方法来获取客户端的IP地址。但在某些状况下,它获取到的结果不必定是你所指望的,这些状况包括:php
那怎样才能获取正确的IP呢?在Laravel中可使用fideloper/proxy
拓展包来解决(本文只讨论Laravel 5.5及之后版本的状况,由于从该版本开始Laravel已经默认集成了该拓展包)。它提供了一个名为App\Http\Middleware\TrustedProxies
的中间件,这个中间件能够帮助你设置可信任代理。比方说你的负载均衡服务器的IP是192.168.1.1
,那你只须要将这个IP配置到$proxies
属性里便可:html
/** * The trusted proxies for this application. * * @var array|string */ protected $proxies = '192.168.1.1';
有些朋友就会问,个人负载均衡服务器IP不固定怎么办(好比AWS的ELB)?这种状况也能解决,可是须要十分谨慎。首先你须要配置你的应用服务器不响应任何非负载均衡过来的请求,这样作的目的是严格控制请求来源,保证所接收到请求是可信的(好比在AWS里面能够经过设置security groups来实现)。而后再将$proxies
设置为*
,表示始终信任上层代理进来的请求,便可。nginx
固然,$proxies
也能够是数组,若是你有多层反向代理,则须要可配置多个IP地址。这里的IP既能够是IPv4也能够是IPv6,而且可使用CIDR风格的IP范围,好比:144.220.0.0/16
。git
我本人就接手过一个项目,它的反向代理比上述状况更复杂:咱们的应用部署在多个AWS云服务器实例之上,并由ELB进行负载均衡,因为该项目有全球访问的需求,咱们在ELB前面还用CloudFront作了CDN加速。前面有介绍ELB的IP是非固定的,而且CloudFront的IP也是非固定。针对这种状况,咱们只能逐一分析。对于ELB层,咱们使用控制请求源并设置$proxies
为*
便可。而对于CloudFront,好在AWS为开发者提供了CloudFront节点服务器的IP范围,因此咱们只要将官网提供的CIDR信息配置到$proxies
属性里面便可。固然CloudFront的IP范围可能随时会改变,因此咱们会定时抓取接口并将结果缓存,以保证准确性和效率。github
了解了如何正确配置TrustedProxies,咱们还要学习原理,知其因此然。分析一下App\Http\Middleware\TrustedProxies
的源码,不难发现,这个中间件最终作的一件事情,就是调用Symfony\Component\HttpFoundation::setTrustedProxies()
方法,将你配置的$proxies
赋值到Symfony\Component\HttpFoundation
类的$trustedProxies
属性中去。看到这你也就明白了,其实这个功能实际是由底层的Symfony提供的,fideloper/proxy
拓展包只是帮忙适配了一下Laravel而已(Symfony大法好呀🤘)。shell
接下来分析源码,打开文件vendor/symfony/http-foundation/Request.php
,阅读一下这个方法:apache
public function getClientIps() { $ip = $this->server->get('REMOTE_ADDR'); if (!$this->isFromTrustedProxy()) { return [$ip]; } return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip]; }
很容易理解,若是你未配置TrustedProxies或者这个请求不是来自可信任的代理,那么就直接返回REMOTE_ADDR
地址,这也是为何获取不到正确IP的缘由。若是这个请求来自可信任代理,就会从X-Forwarded-For
头中获取客户端的IP。数组
首先认识一下REMOTE_ADDR
,它是服务器(nginx/apache)与客户端进行TCP链接时获取的真实客户端地址,是不可伪造的。好比你使用了负载均衡,那么在应用里得到的REMOTE_ADDR
就是负载均衡服务器的地址,不然就是客户机的地址。因此isFromTrustedProxy()
方法也是基于REMOTE_ADDR
来作判断的。缓存
而后是X-Forwarded-For
,它是HTTP协议里常见的一个拓展头,用于记录从客户端到应用服务器之间所通过的代理服务器或者负载均衡的地址,包括客户端地址。格式以下:安全
X-Forwarded-For: client, proxy1, proxy2, proxy3
每一层代理服务器都会将上一层代理的地址追加到这个头里面来,也就是咱们常在nginx配置文件中见到的这项配置:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
因此想要获取到真实的客户端IP,就须要经过这个头部来获取。但须要注意的是,X-Forwarded-For
是能够被随意伪造的,比方说我随意构造一个HTTP请求:
$ curl -H "X-Forwarded-For: 192.168.1.1, 192.168.1.2, 192.168.1.3" https://example.com
正由于这种可伪造性,致使咱们不能直接使用X-Forwarded-For
里的第一个IP做为最终结果。不用担忧,Symfony已经帮咱们处理了这一切。关于Symfony具体的作法,感兴趣的朋友能够直接查看getTrustedValues()
方法的源码,我大体描述一下过程:
首先从HTTP头部中取出X-Forwarded-For
和Forwarded
的值生成IP列表。这里为何会去取Forwarded
头呢?事实上X-Forwarded-For
目前不属于任何一份既有规范,这个消息首部的标准版本是Forwarded
,格式以下:
Forwarded: by=<identifier>; for=<identifier>; host=<host>; proto=<http|https>
而Symfony兼顾了两种头部格式的处理,但若是这两头同时存在Symfony会抛出冲突异常,你能够经过设置Trusted Header移除其中一个来避免冲突异常。拿到IP列表后,再经过normalizeAndFilterClientIps()
方法来滤出客户端IP列表。normalizeAndFilterClientIps()
方法会将输入的IP一个一个地判断是否为开发者配置的可信任IP,若是是则从列表中移除,剩余的则是客户端IP列表。但特别重要的一点是,normalizeAndFilterClientIps()
方法在返回结果的时候会调用array_reverse()
方法将客户端IP列表进行逆序。也许你会有疑问,为何要将结果逆序返回呢?明明协议中规定第一个才是“真实”的客户端IP,但偏偏是这个逆序,才保证告终果的安全。咱们来举个实例就明白了:
假设咱们服务器的反向代理链条是这样的:192.168.66.1 -> 192.168.66.2 -> 192.168.66.3
,最后一个是应用服务器IP,而且咱们的程序中已将192.168.66.1
、192.168.66.2
添加到了可信任代理中。这时有个恶意用户访问了咱们的站点,他的主机IP是192.168.1.1
,他在访问咱们的站点时构造了X-Forwarded-For
:
$ curl -H "X-Forwarded-For: 192.168.1.3, 192.168.1.2" https://example.com
这个恶意请求最终到达应用服务器后的X-Forwarded-For
其实是这样的:
X-Forwarded-For: 192.168.1.3, 192.168.1.2, 192.168.1.1, 192.168.66.1
程序在normalizeAndFilterClientIps()
方法过滤掉可信任代理IP后,剩余的结果为:192.168.1.3, 192.168.1.2, 192.168.1.1
。很显然,若是不进行逆序处理,咱们使用Illuminate\Http\Request::ip()
获取到的IP则是恶意用户构造的192.168.1.3
,而逆序处理后得到的IP则是真实的192.168.1.1
。因此这个逆序很关键。
了解上述原理之后,即便你不使用Laravel或者Symfony框架,也能够在本身的项目中实现正确的逻辑,而不是从某度CV一段错误的代码,让本身的应用面临风险。
有些开发者喜欢讲将配置统一到config/
目录下,而不是直接在中间件中进行配置,你只须要运行如下命令,就能够发布配置文件trustedproxies.php
:
$ php artisan vendor:publish --provider="Fideloper\Proxy\TrustedProxyServiceProvider"
固然,若是你有分环境配置的需求,可自行使用env()
方法进行拓展。可是请注意,中间件里的$proxies
属性是优先于配置文件的,当$proxies
属性有值的时候,配置文件里设置的值将失效,请勿踩坑。