在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
屬性有值的時候,配置文件裏設置的值將失效,請勿踩坑。