一、背景
1. 業(yè)務(wù)上云帶來(lái)性能收益
公司從去年全面推動(dòng)業(yè)務(wù)上云,而以往 IDC 架構(gòu)部署上,接入層采用典型的 4 層 LVS 多機(jī)房容災(zāi)架構(gòu),在業(yè)務(wù)高峰時(shí)期,擴(kuò)容困難(受限于物理機(jī)資源和 LVS 內(nèi)網(wǎng)網(wǎng)段的網(wǎng)絡(luò)規(guī)劃),且抵擋不住 HTTPS 卸載引發(fā)的高 CPU 占用。
而經(jīng)過(guò)壓力測(cè)試發(fā)現(xiàn),使用騰訊云 7 層 CLB 負(fù)載均衡進(jìn)行 HTTPS 卸載,性能得到極大提升。測(cè)試數(shù)據(jù)也表明,IDC 舊架構(gòu)中,啟用 HTTPS 會(huì)帶來(lái) 90% 以上的性能損耗。
2. 架構(gòu)調(diào)整引發(fā)多次故障
引入騰訊云 7 層 CLB 負(fù)載均衡產(chǎn)品,帶了了巨大的性能提升,卻也給業(yè)務(wù)帶來(lái)了痛苦,主要核心問(wèn)題是獲取客戶端的真實(shí) IP 上。
當(dāng)前現(xiàn)狀是業(yè)務(wù)語(yǔ)言異構(gòu)(PHP + Go),多數(shù)業(yè)務(wù)已經(jīng)歷服務(wù)化改造,但缺乏服務(wù)發(fā)現(xiàn)機(jī)制,服務(wù)與服務(wù)之間的調(diào)用依賴域名和 DNS 解析,大部分都是 HTTP 服務(wù)。
在架構(gòu)調(diào)整后,由于未能 100% 覆蓋測(cè)試,導(dǎo)致漏測(cè)的服務(wù)經(jīng)常拿到錯(cuò)誤的客戶端 IP 地址,造成的后果是損失大量的用戶。這些用戶會(huì)因?yàn)槎绦膨?yàn)證碼發(fā)送限制、IP 登錄頻次過(guò)高而無(wú)法登錄、充值,給公司帶來(lái)巨大損失。
3. 未來(lái)的路應(yīng)該怎么走?
更進(jìn)一步講,當(dāng)前業(yè)務(wù)如何抵擋外界的 DDoS 攻擊、請(qǐng)求機(jī)器人、SQL 注入等等,最簡(jiǎn)單的是接入高防 IP、WAF 應(yīng)用防火墻,而請(qǐng)求經(jīng)過(guò)多輪轉(zhuǎn)發(fā),同樣也有獲取客戶端真實(shí) IP 的問(wèn)題。
再者,業(yè)務(wù)也在逐步容器化,享受 Kubernetes 彈性擴(kuò)容的便利,怎么平滑遷移也是非常值得深思的。
假設(shè)有一天某個(gè)同學(xué),不小心配置有誤——應(yīng)用層拿到的,很有可能是高防 IP 或者 WAF 的 IP,業(yè)務(wù)絕對(duì)無(wú)法忍受。
顯然,確定一個(gè)業(yè)務(wù)無(wú)感知的方案并成功落地迫在眉睫。
然而翻遍整個(gè)互聯(lián)網(wǎng),幾乎沒(méi)有文章能把這些看起來(lái)很簡(jiǎn)單的事情捋清楚、講明白,更不用說(shuō)最佳實(shí)踐。
大多數(shù)人都是抄抄配置,潦潦草草上線,方案并沒(méi)有普適性。
這篇文章也是我在這段時(shí)間的研究中總結(jié)出來(lái)的寶貴經(jīng)驗(yàn),希望對(duì)讀者能有些許幫助。文章篇幅較長(zhǎng),難免有錯(cuò)誤之處,還請(qǐng)各位看官斧正,感激不盡:)
二、名詞釋義
1. REMOTE-ADDR
- Nginx + PHP 模式下,REMOTE-ADDR 為遠(yuǎn)端的 IP 地址,可通過(guò)
$_SERVER['REMOTE-ADDR']
獲?。?/li> - 它代表與上一層建立 TCP 連接的 IP 地址;
- 網(wǎng)站無(wú)代理時(shí)(客戶端->服務(wù)端),WEB服務(wù)器(Nginx,Apache等)會(huì)設(shè)置該值為客戶端 IP;
- 網(wǎng)站存在代理時(shí)(客戶端->代理->服務(wù)端),該值為代理的 IP。
proxy_set_header REMOTE-ADDR $remote_addr;
2. X-Forwarded-For
X-Forwarded-For 是一個(gè) HTTP 擴(kuò)展頭部。HTTP/1.1(RFC 2616)協(xié)議并沒(méi)有對(duì)它的定義,它最開(kāi)始是由 Squid 這個(gè)緩存代理軟件引入,用來(lái)表示 HTTP 請(qǐng)求端真實(shí) IP。如今它已經(jīng)成為事實(shí)上的標(biāo)準(zhǔn),被各大 HTTP 代理、負(fù)載均衡等轉(zhuǎn)發(fā)服務(wù)廣泛使用,并被寫(xiě)入 RFC 7239(Forwarded HTTP Extension)標(biāo)準(zhǔn)之中。
-
格式為英文逗號(hào) + 空格隔開(kāi),例如:X-Forwarded-For: IP0(client), IP1(proxy), IP2(proxy);
-
中間經(jīng)過(guò)的代理,會(huì)逐層追加至末尾;
-
IP0 離服務(wù)端最遠(yuǎn),然后是每一級(jí)代理設(shè)備的 IP,IP2 直連服務(wù)端。
-
如果客戶端偽造 IP 地址,格式為:X-Forwarded-For: 偽造的 IP 地址 1, [偽造的 IP 地址 2…], IP0(client), IP1(proxy), IP2(proxy)。
3. X-Real-IP
注:CLB <=> SLB,為騰訊云和阿里云不同產(chǎn)品的稱(chēng)呼,均為負(fù)載均衡。
典型的調(diào)用鏈路:
client --> ① [CLB-7]gateway --域名--> ② [CLB-7]server(③ nginx + ④ go/php)
- X-Real-IP 為建立 TCP 連接的上一跳的 IP 地址;
- 對(duì)于 ④ 而言,X-Real-IP 為 ① 網(wǎng)關(guān)的 NAT 公網(wǎng)出口 IP 地址,或 gateway 的內(nèi)網(wǎng) IP 地址,該結(jié)論通過(guò)生產(chǎn)環(huán)境 tcpdump 抓包驗(yàn)證得到;
- 公網(wǎng)調(diào)用下,① 網(wǎng)關(guān) 調(diào)用 ② 7 層 CLB,再到應(yīng)用層 ③④,此時(shí) ④ 拿到的 X-Real-IP 為 ① 的 NAT 公網(wǎng)出口地址(7 層 CLB 會(huì)重寫(xiě) X-Real-IP 頭部,并追加 X-Forwarded-For 頭部);
- 內(nèi)網(wǎng)環(huán)境中,原理相似,只不過(guò)拿到的是 gateway 的內(nèi)網(wǎng) IP 地址;
- 中間可能被 ③ nginx 重寫(xiě),此時(shí)等同于 REMOTE-ADDR。
比如以下最常見(jiàn)的 nginx 配置:
proxy_set_header REMOTE-ADDR $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
REMOTE-ADDR 和 X-Real-IP 都是 nginx 的 $remote_addr 變量,再傳遞給下游。
三、面臨困境
1. 運(yùn)維側(cè)
- 業(yè)務(wù)線配置五花八門(mén),沒(méi)有統(tǒng)一。具體表現(xiàn)在 nginx.conf 和 vhost 配置在不同的業(yè)務(wù)線有很大區(qū)別;
- vhost 成千上萬(wàn),nginx 內(nèi)部存在多重轉(zhuǎn)發(fā),外部也有網(wǎng)關(guān)轉(zhuǎn)發(fā)過(guò)來(lái)的流量,且網(wǎng)關(guān)不止一套,捋不清鏈路容易導(dǎo)致線上故障;
- 缺乏完善的 QA 驗(yàn)證流程,變更沒(méi)辦法 100% 覆蓋測(cè)試,最終結(jié)果就是盡可能少變更,但這不是長(zhǎng)久之計(jì);
- 存在開(kāi)發(fā)自行維護(hù)信任 IP 的情況,所以運(yùn)維不敢隨便變更,因?yàn)樽兏靶枰ㄖ_(kāi)發(fā)整改,開(kāi)發(fā)有自己的時(shí)間排期,處理起來(lái)效率極其低下;
- 為了盡可能少修改原先的配置,部分機(jī)器組接入了騰訊云的 TOA 模塊,用來(lái)獲取客戶端真實(shí) IP 地址,而阿里云沒(méi)有相似的產(chǎn)品,如果沒(méi)有統(tǒng)一的方案,沒(méi)辦法上線阿里云,實(shí)現(xiàn)不了雙云雙活的目標(biāo)等等。
2. 開(kāi)發(fā)側(cè)
各個(gè)業(yè)務(wù)線使用的技術(shù)棧不統(tǒng)一,存在多種獲取客戶端 IP 的方案,需要找到一種盡可能少修改代碼,或者一點(diǎn)都不需要修改代碼的方案。
PHP 以 Laravel 框架為例(底層是 Symfony 框架),發(fā)現(xiàn)內(nèi)部取了 $_SERVER[‘REMOTE_ADDR’] 變量:
public function getClientIp()
{
$ipAddresses = $this->getClientIps();
return $ipAddresses[0]; // 1. 取第一個(gè) IP 地址。
}
public function getClientIps()
{
$ip = $this->server->get('REMOTE_ADDR');
if (!$this->isFromTrustedProxy()) {
// 2. 程序在這里返回了 REMOTE_ADDR 頭部的值。
return [$ip];
}
// 3. 永遠(yuǎn)到不了這個(gè)分支。
return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
}
public function isFromTrustedProxy()
{
// 4. 因?yàn)樯a(chǎn)環(huán)境中,$trustedProxies 沒(méi)有配置。
return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR', ''), self::$trustedProxies);
}
公司內(nèi)部有些業(yè)務(wù)自己實(shí)現(xiàn)函數(shù),依賴的是 X-Forwarded-For 頭部。
Go 以 Gin 框架為例,準(zhǔn)確的說(shuō)是 Gin@v1.6. 版本,它先取 X-Forwarded-For 的第一個(gè) IP,取不到就取 X-Real-IP 頭部:*
func (c *Context) ClientIP() string {
// 1. ForwardedByClientIP 默認(rèn)為 true
if c.engine.ForwardedByClientIP {
// 2. 優(yōu)先獲取 X-Forwarded-For 頭部
clientIP := c.requestHeader("X-Forwarded-For")
// 3. 取 X-Forwarded-For 的第一個(gè) IP 地址
clientIP = strings.TrimSpace(strings.Split(clientIP, ",")[0])
// 4. 取不到就取 X-Real-Ip 字段
if clientIP == "" {
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
}
// 5. 拿到了就直接返回(正常的邏輯)
if clientIP != "" {
return clientIP
}
}
// 6. 忽略,該值為 false,除非 build tags 包含 appengine 為 true
if c.engine.AppEngine {
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr
}
}
// 7. 以上都取不到的話,取 RemoteAddr 字段,走到這個(gè)邏輯,程序肯定不正常。
// 參考 Go 標(biāo)準(zhǔn)庫(kù),該值為 TCP 建立連接的遠(yuǎn)端 IP 地址
// go1.17.1/src/net/http/server.go:1003
// req.RemoteAddr = *conn.remoteAddr
if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
return ip
}
return ""
}
經(jīng)過(guò)調(diào)研發(fā)現(xiàn),業(yè)務(wù)取的是 X-Real-IP 字段,具體原因就不展開(kāi)了。
至于 Gin@1.7.* 版本,由于 Gin@1.6.* 的實(shí)現(xiàn)存在偽造客戶端 IP 的問(wèn)題,被爆 CVE-2020-28483 漏洞,官方為了修復(fù)這個(gè)問(wèn)題,換了一種實(shí)現(xiàn)修復(fù)該漏洞:
func (c *Context) ClientIP() string {
// 1. 自定義 Header 的情況,可以忽略
if c.engine.TrustedPlatform != "" {
if addr := c.requestHeader(c.engine.TrustedPlatform); addr != "" {
return addr
}
}
if c.engine.AppEngine {
if addr := c.requestHeader("X-Appengine-Remote-Addr"); addr != "" {
return addr
}
}
// 2. 獲取 IP 地址,并返回是否可以信任
remoteIP, trusted := c.RemoteIP()
if remoteIP == nil {
return ""
}
// 3. 如果信任,檢查 IP 地址的合法性,合法就返回
// 默認(rèn)值:ForwardedByClientIP=true,RemoteIPHeaders=[X-Forwarded-For(優(yōu)先), X-Real-IP]
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {
// c.requestHeader 在頭部有效的情況下,也是返回第一個(gè) IP 地址。
ip, valid := validateHeader(c.requestHeader(headerName))
if valid {
return ip
}
}
}
// 4. 不能信任,那就用 TCP 連接遠(yuǎn)端 IP 兜底。
return remoteIP.String()
}
func (c *Context) RemoteIP() (net.IP, bool) {
ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr))
if err != nil {
return nil, false
}
remoteIP := net.ParseIP(ip)
if remoteIP == nil {
return nil, false
}
// remoteIP = TCP 連接遠(yuǎn)端 IP 地址
// 由于業(yè)務(wù)沒(méi)有配置 engine.TrustedProxies,所以是不可信任的。
return remoteIP, c.engine.isTrustedProxy(remoteIP)
}
func (e *Engine) isTrustedProxy(ip net.IP) bool {
if e.trustedCIDRs != nil {
for _, cidr := range e.trustedCIDRs {
if cidr.Contains(ip) {
return true
}
}
}
// 業(yè)務(wù)將會(huì)走到這里!
return false
}
func (e *Engine) validateHeader(header string) (clientIP string, valid bool) {
if header == "" {
return "", false
}
items := strings.Split(header, ",")
for i := len(items) - 1; i >= 0; i-- {
ipStr := strings.TrimSpace(items[i])
ip := net.ParseIP(ipStr)
if ip == nil {
return "", false
}
// X-Forwarded-For is appended by proxy
// Check IPs in reverse order and stop when find untrusted proxy
if (i == 0) || (!e.isTrustedProxy(ip)) {
return ipStr, true
}
}
return
}
官方的手法也是簡(jiǎn)單粗暴,以前是將錯(cuò)就錯(cuò),這次一下子修復(fù)好了,搞得很多人翻車(chē)了(https://github.com/gin-gonic/gin/issues/2697)。
原因是新的實(shí)現(xiàn)沒(méi)有兼容 1.6 版本,導(dǎo)致升級(jí)框架后獲取不到客戶端的真實(shí) IP,1.7.7 才解決該問(wèn)題。
四、三大原則
分析完整個(gè)事情的來(lái)龍去脈,想必讀者們對(duì)現(xiàn)狀有一定的了解。
我把這套方案,抽象為三大原則,只要理解它,獲取客戶端真實(shí) IP 的問(wèn)題,就跟喝水一樣簡(jiǎn)單!
1. 代理必須向下傳遞客戶端 IP 地址
原因:從入口流量開(kāi)始,經(jīng)過(guò) N 層代理,如果代理中間不傳遞客戶端的 IP 地址,底層業(yè)務(wù)必然獲取不到客戶端的真實(shí) IP 地址。
2. 統(tǒng)一使用 nginx 的 realip 模塊獲取客戶端 IP 地址
# nginx.conf
# ...
set_real_ip_from 騰訊云/阿里云 NAT 出口網(wǎng)段;
set_real_ip_from 騰訊云/阿里云高防 IP 網(wǎng)段;
set_real_ip_from 騰訊云/阿里云 WAF 網(wǎng)段;
set_real_ip_from CDN 網(wǎng)段;
set_real_ip_from 內(nèi)網(wǎng)地址網(wǎng)段; # 按需配置,對(duì)于網(wǎng)關(guān)進(jìn)來(lái)的請(qǐng)求通過(guò)內(nèi)網(wǎng)到業(yè)務(wù)機(jī)器,需要配置上這個(gè)網(wǎng)段。
set_real_ip_from 127.0.0.1; # 按需配置,主要作用在 nginx 的內(nèi)部轉(zhuǎn)發(fā)。
real_ip_header X-Forwarded-For;
real_ip_recursive on; # 必須打開(kāi)該選項(xiàng),原因見(jiàn)下面分析。
access_by_lua '
ngx.req.set_header("X-REAL-IP", ngx.var.remote_addr)
ngx.req.set_header("X-FORWARDED-FOR", ngx.var.remote_addr)
';
# vhost/*.conf
location ^~ /foo {
access_log logs/api_foo.access.log main;
proxy_pass http://api_foo;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Connection "";
}
此時(shí),X-Real-IP、REMOTE-ADDR、X-Forwarded-For 均統(tǒng)一為 realip 模塊重寫(xiě)后的 $remote_addr
變量,業(yè)務(wù)就可以取到真實(shí)的客戶端 IP 地址,無(wú)需考慮 PHP、Go 等不同語(yǔ)言、同種語(yǔ)言不同框架下的差異。
那問(wèn)題來(lái)了,客戶端 IP 是否會(huì)被偽造?答案是不會(huì)的。
按照 X-Forwarded-For 的定義,該頭部每經(jīng)過(guò)一層就追加一個(gè) IP 地址:
X-Forwarded-For: 客戶端偽造 IP 地址, IP0(client), IP1(proxy), IP2(proxy)
那么,我們只需啟用 realip 模塊的 real_ip_recursive 遞歸模式,將從右往左逐步剔除 IP2,IP1 等信任代理,最后會(huì)獲取到真實(shí)的客戶端 IP 地址。
問(wèn)題二:網(wǎng)上有一種邊緣節(jié)點(diǎn)的方案,為什么不采用?
邊緣節(jié)點(diǎn),指的就是接入層,直接連接客戶端的那一層。經(jīng)過(guò)邊緣節(jié)點(diǎn)轉(zhuǎn)發(fā)到下游的,統(tǒng)稱(chēng)為非邊緣節(jié)點(diǎn)。
按照這個(gè)思路,如果邊緣節(jié)點(diǎn)拿到了客戶端 IP,重置 X-FORWARDED-FOR 頭部為客戶端 IP 地址,并轉(zhuǎn)發(fā)到下游,業(yè)務(wù)只獲取第一個(gè) IP 地址,理論上也不會(huì)被偽造,業(yè)務(wù)也簡(jiǎn)單,為什么不采用?
因?yàn)檫吘壒?jié)點(diǎn)方案最大的缺點(diǎn)在于失去了靈活性,譬如你想接入高防 IP 或者 WAF 防火墻,此時(shí)它已不再是邊緣節(jié)點(diǎn),而是接收高防服務(wù)器或 WAF 防火墻清洗的流量,將會(huì)拿到錯(cuò)誤的 IP 地址。
3. 運(yùn)維維護(hù)信任 IP 列表,開(kāi)發(fā)代碼不做處理
由 2 可知,三個(gè)頭部均為統(tǒng)一的值,對(duì)開(kāi)發(fā)可以保證最大的兼容性。原因是不同的語(yǔ)言,同個(gè)語(yǔ)言的不同開(kāi)發(fā)框架,同個(gè)框架的不同版本,獲取客戶端 IP 的方式也就這幾種。
對(duì)開(kāi)發(fā)而言,確實(shí)沒(méi)必要關(guān)心自己的代碼需要引入 NAT 網(wǎng)關(guān) IP 配置、高防 IP 配置等,并且每個(gè)工程可能都要修改,這是不現(xiàn)實(shí)的。
本質(zhì)上,這也是運(yùn)維的工作。舉個(gè)例子,如果真的遇到 DDoS 攻擊,切換高防 IP 抵御 DDoS 攻擊的操作人是運(yùn)維,開(kāi)發(fā)這個(gè)時(shí)候去將所有工程配置上高防 IP 地址是一件極其痛苦的事情。一旦加漏、加錯(cuò)將直接引發(fā)故障。
五、最佳實(shí)踐
(1) 虛擬機(jī)部署
- SRE 維護(hù)信任的 IP 池,X-Real-IP、REMOTE-ADDR、X-Forwarded-For 均統(tǒng)一為 realip 模塊重寫(xiě)后的 $remote_addr 變量,開(kāi)發(fā)不感知;
- 開(kāi)發(fā)無(wú)需修改代碼,因?yàn)樯鲜鋈齻€(gè)變量讀取出來(lái)的值是一致的,無(wú)任何風(fēng)險(xiǎn)。
(2) 容器化部署
a. PHP 無(wú)需改動(dòng),可以平滑切換上容器。因?yàn)?PHP 容器上層依然有 nginx.conf,平移該配置即可;
b. GO 容器化,有 2 種方案:
注:最終采用方案 2,去除了 Pod 內(nèi)部的 nginx 轉(zhuǎn)發(fā),Pod 的上層使用了 nginx-ingress,做到了業(yè)務(wù)無(wú)感知容器上云。
-
如果保留虛擬機(jī)架構(gòu),即 Go 服務(wù)上層有 nginx,也是平移就可以了,跟 PHP 一樣;
-
如果 Go 服務(wù)上游去除 nginx 轉(zhuǎn)發(fā):
流量入口使用 7 層騰訊云 CLB / 阿里云 SLB 進(jìn)行 HTTPS 卸載后轉(zhuǎn)發(fā)到容器集群的 nginx-ingress,業(yè)務(wù)代碼無(wú)感知。實(shí)現(xiàn)原理和虛擬機(jī)方案相似,均為配置 realip 模塊和統(tǒng)一 X-Real-IP、REMOTE-ADDR、X-Forwarded-For 頭部,詳情可以參考以下資料:
- https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#use-proxy-protocol
- https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#proxy-real-ip-cidr
- https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#server-snippet
- https://help.aliyun.com/document_detail/86533.html
還有個(gè)容易忽略的點(diǎn)——ingress 選型。
如果使用 Pod 直連,也就是不使用 nginx-ingress:
PHP / Go 上層都需要有一層 nginx 并配置好 nginx.conf,配置 realip 模塊和統(tǒng)一 X-Real-IP、REMOTE-ADDR、X-Forwarded-For 頭部。
此時(shí) PHP / Go 架構(gòu)統(tǒng)一,但對(duì) Go 容器來(lái)說(shuō)多了一層 nginx,會(huì)造成資源浪費(fèi)(每個(gè) Pod 都需要部署一個(gè) nginx,再轉(zhuǎn)發(fā)到 Go)。
具體用哪個(gè) ingress,就要看怎么取舍了。
nginx 存在的意義在于阻止業(yè)務(wù)直接感知到信任代理 IP 列表的存在,如果對(duì)于你的業(yè)務(wù)而言,各個(gè)業(yè)務(wù)線去維護(hù)這個(gè)配置列表成本極低,那 nginx 確實(shí)是沒(méi)有存在的必要性。
總之,我個(gè)人認(rèn)為:
- 業(yè)務(wù)完全不需要關(guān)心如何獲取客戶端的真實(shí) IP,這是最好的選擇;
- 千萬(wàn)不要封裝各種函數(shù)去獲取客戶端真實(shí) IP,這種問(wèn)題最好交給上層 SRE 基礎(chǔ)架構(gòu)的同學(xué)負(fù)責(zé),不然真的非常容易出問(wèn)題;
- 理解好三大原則,獲取客戶端真實(shí) IP 的問(wèn)題,就跟喝水一樣簡(jiǎn)單!
OK,文章終于寫(xiě)完了,花費(fèi)了好多天的時(shí)間整理,憋出來(lái)了。感謝你讀到這里,是時(shí)候吃晚飯了:)文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-753849.html
文章來(lái)源于本人博客,發(fā)布于 2021-12-19,原文鏈接:https://imlht.com/archives/248/文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-753849.html
到了這里,關(guān)于獲取客戶端真實(shí) IP 地址的最佳實(shí)踐的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!