Laravel獲取客戶端IP懊蒸,你的姿勢對嗎

在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-ForForwarded的值生成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屬性有值的時候种冬,配置文件里設置的值將失效镣丑,請勿踩坑。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末娱两,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子金吗,更是在濱河造成了極大的恐慌十兢,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摇庙,死亡現場離奇詭異旱物,居然都是意外死亡,警方通過查閱死者的電腦和手機卫袒,發(fā)現死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門宵呛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人夕凝,你說我怎么就攤上這事宝穗。” “怎么了码秉?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵逮矛,是天一觀的道長。 經常有香客問我转砖,道長须鼎,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任府蔗,我火速辦了婚禮晋控,結果婚禮上,老公的妹妹穿的比我還像新娘姓赤。我一直安慰自己赡译,他們只是感情好,可當我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布模捂。 她就那樣靜靜地躺著捶朵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狂男。 梳的紋絲不亂的頭發(fā)上综看,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天,我揣著相機與錄音岖食,去河邊找鬼红碑。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的析珊。 我是一名探鬼主播羡鸥,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼忠寻!你這毒婦竟也來了惧浴?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤奕剃,失蹤者是張志新(化名)和其女友劉穎衷旅,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體纵朋,經...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡柿顶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了操软。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘁锯。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖聂薪,靈堂內的尸體忽然破棺而出家乘,到底是詐尸還是另有隱情,我是刑警寧澤胆建,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布烤低,位于F島的核電站,受9級特大地震影響笆载,放射性物質發(fā)生泄漏扑馁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一凉驻、第九天 我趴在偏房一處隱蔽的房頂上張望腻要。 院中可真熱鬧,春花似錦涝登、人聲如沸雄家。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽趟济。三九已至,卻和暖如春咽笼,著一層夾襖步出監(jiān)牢的瞬間顷编,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工剑刑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留媳纬,地道東北人双肤。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像钮惠,于是被迫代替她去往敵國和親茅糜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,884評論 2 354

推薦閱讀更多精彩內容