在Laravel中通常使用Illuminate\Http\Request::ip()
方法來獲取客戶端的IP地址。但在某些情況下悯搔,它獲取到的結果不一定是你所期望的骑丸,這些情況包括:
- 你的應用部署在負載均衡后面
- 你的應用使用了CDN加速
- 你的應用部署在其它反向代理后面
那怎樣才能獲取正確的IP呢?在Laravel中可以使用fideloper/proxy
拓展包來解決(本文只討論Laravel 5.5及以后版本的情況妒貌,因為從該版本開始Laravel已經默認集成了該拓展包)通危。它提供了一個名為App\Http\Middleware\TrustedProxies
的中間件,這個中間件可以幫助你設置可信任代理灌曙。比方說你的負載均衡服務器的IP是192.168.1.1
菊碟,那你只需要將這個IP配置到$proxies
屬性里即可:
/**
* The trusted proxies for this application.
*
* @var array|string
*/
protected $proxies = '192.168.1.1';
有些朋友就會問,我的負載均衡服務器IP不固定怎么辦(比如AWS的ELB)在刺?這種情況也能解決逆害,但是需要十分謹慎头镊。首先你需要配置你的應用服務器不響應任何非負載均衡過來的請求,這樣做的目的是嚴格控制請求來源魄幕,保證所接收到請求是可信的(比如在AWS里面可以通過設置security groups來實現)相艇。然后再將$proxies
設置為*
,表示始終信任上層代理進來的請求梅垄,即可厂捞。
當然,$proxies
也可以是數組队丝,如果你有多層反向代理靡馁,則需要可配置多個IP地址。這里的IP既可以是IPv4也可以是IPv6机久,并且可以使用CIDR風格的IP范圍臭墨,比如:144.220.0.0/16
。
我本人就接手過一個項目膘盖,它的反向代理比上述情況更復雜:我們的應用部署在多個AWS云服務器實例之上胧弛,并由ELB進行負載均衡,由于該項目有全球訪問的需求侠畔,我們在ELB前面還用CloudFront做了CDN加速结缚。前面有介紹ELB的IP是非固定的,并且CloudFront的IP也是非固定软棺。針對這種情況红竭,我們只能逐一分析。對于ELB層喘落,我們使用控制請求源并設置$proxies
為*
即可茵宪。而對于CloudFront,好在AWS為開發(fā)者提供了CloudFront節(jié)點服務器的IP范圍瘦棋,所以我們只要將官網提供的CIDR信息配置到$proxies
屬性里面即可稀火。當然CloudFront的IP范圍可能隨時會改變,所以我們會定時抓取接口并將結果緩存赌朋,以保證準確性和效率凰狞。
原理
了解了如何正確配置TrustedProxies,我們還要學習原理沛慢,知其所以然服球。分析一下App\Http\Middleware\TrustedProxies
的源碼,不難發(fā)現颠焦,這個中間件最終做的一件事情,就是調用Symfony\Component\HttpFoundation::setTrustedProxies()
方法往枣,將你配置的$proxies
賦值到Symfony\Component\HttpFoundation
類的$trustedProxies
屬性中去伐庭》矍看到這你也就明白了,其實這個功能實際是由底層的Symfony提供的圾另,fideloper/proxy
拓展包只是幫忙適配了一下Laravel而已(Symfony大法好呀??)霸株。
接下來分析源碼,打開文件vendor/symfony/http-foundation/Request.php
集乔,閱讀一下這個方法:
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
目前不屬于任何一份既有規(guī)范铣墨,這個消息首部的標準版本是Forwarded
,格式如下:
Forwarded: by=<identifier>; for=<identifier>; host=<host>; proto=<http|https>
而Symfony兼顧了兩種頭部格式的處理办绝,但如果這兩頭同時存在Symfony會拋出沖突異常伊约,你可以通過設置Trusted Header移除其中一個來避免沖突異常姚淆。拿到IP列表后,再通過normalizeAndFilterClientIps()
方法來濾出客戶端IP列表屡律。normalizeAndFilterClientIps()
方法會將輸入的IP一個一個地判斷是否為開發(fā)者配置的可信任IP腌逢,如果是則從列表中移除,剩余的則是客戶端IP列表超埋。但特別重要的一點是搏讶,normalizeAndFilterClientIps()
方法在返回結果的時候會調用array_reverse()
方法將客戶端IP列表進行逆序。也許你會有疑問霍殴,為什么要將結果逆序返回呢媒惕?明明協議中規(guī)定第一個才是“真實”的客戶端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一段錯誤的代碼肢专,讓自己的應用面臨風險舞肆。
配置文件
有些開發(fā)者喜歡講將配置統(tǒng)一到config/
目錄下,而不是直接在中間件中進行配置博杖,你只需要運行以下命令椿胯,就可以發(fā)布配置文件trustedproxies.php
:
$ php artisan vendor:publish --provider="Fideloper\Proxy\TrustedProxyServiceProvider"
當然,如果你有分環(huán)境配置的需求剃根,可自行使用env()
方法進行拓展哩盲。但是請注意,中間件里的$proxies
屬性是優(yōu)先于配置文件的,當$proxies
屬性有值的時候种冬,配置文件里設置的值將失效镣丑,請勿踩坑。