架設 Web 服務的時候,「取得使用者的真實 IP」聽起來是一件很基本的事,但只要架構從單機變成多層代理,這件事就會開始讓人困惑。直接跑在 AP Server 上的程式拿到的 remoteAddress 通常是 127.0.0.1,因為連到 AP 的是同一台機器上的 Nginx,不是外部的使用者。如果前面還多一層 Cloudflare,Nginx 自己收到的 IP 也不是使用者的,而是 Cloudflare 的節點 IP。
搞混這些 IP 會讓 Rate Limit 對整個 Cloudflare 限流(等於把所有使用者一起擋)、讓 access log 記錄的全是內部 IP(出事的時候查不到來源)、讓 geo-blocking 判斷的是 CDN 節點的位置而不是使用者的位置。這篇文章會用圖解拆開請求經過每一層時 header 的變化,搞清楚 AP 收到的每個 IP 代表什麼,最後用 Node.js、Python、Java、C# 四種語言示範怎麼正確取得真實 IP。
三層架構的請求流程
典型的部署方式是 Cloudflare 擋在最前面做 CDN 和 DDoS 防護,後面接 Nginx 做 反向代理 和 TLS 終結,最後才是跑業務邏輯的 Application Server。每一層收到請求時,TCP 連線的來源 IP(也就是 socket 層的 remote address)永遠是「上一層」的 IP,不是原始使用者的 IP。

這就是為什麼直接讀 socket IP 拿到的永遠是上一層的位址。要拿到使用者的真實 IP,必須依賴每一層在 HTTP header 裡留下的資訊。
圖中把 Nginx 和 AP 畫在同一台機器上(127.0.0.1)是常見的配法,但實務上 Nginx 和 AP 可以分開部署在不同主機,這時候 AP 收到的 socket IP 會是 Nginx 那台的內網 IP(例如 10.0.1.5)。更進一步,Nginx 也可以作為負載平衡器,用 upstream 把請求分發到多台 AP Server,形成一對多的關係。不管是哪種部署方式,IP header 的運作原理都一樣,差別在 socket IP 的值會隨著實際網路拓撲而改變。
AP Server 會收到的四種 IP

在 Cloudflare → Nginx → AP 這條路徑上,AP 最終可能會看到四種不同的 IP 來源,每個代表的意義不一樣:
| 來源 | 值(以上方範例) | 代表什麼 |
|---|---|---|
Socket IP(remoteAddress) | 127.0.0.1 | 直接連上 AP 的那一層,通常是 Nginx |
X-Forwarded-For header | 203.0.113.50, 172.64.200.1 | 整條代理鏈經過的 IP,由左到右依序附加 |
X-Real-IP header | 203.0.113.50 | Nginx 設定的單一 IP,通常是真實使用者 IP |
CF-Connecting-IP header | 203.0.113.50 | Cloudflare 提供的使用者 IP,不可被客戶端偽造 |
Socket IP:永遠是上一層
Socket IP 是 TCP 連線層面的來源位址,由作業系統核心在 accept() 時填入,應用程式無法偽造(除非中間有 NAT 或 proxy protocol)。在 Nginx 和 AP 跑在同一台機器的典型配置下,AP 拿到的 socket IP 就是 127.0.0.1。如果 Nginx 和 AP 分開部署在不同主機,這裡會是 Nginx 那台機器的內網 IP,例如 10.0.1.5。
對應到各語言的 API:Node.js 的 req.socket.remoteAddress、Python Flask 的 request.remote_addr、Java Servlet 的 request.getRemoteAddr()、C# ASP.NET Core 的 HttpContext.Connection.RemoteIpAddress。這些在反向代理架構下拿到的都是 Nginx 的 IP,不是使用者的。
X-Forwarded-For:代理鏈的完整紀錄
X-Forwarded-For(簡稱 XFF)是業界沿用最久的 header,用來記錄請求經過了哪些代理。每經過一層代理,這層代理會把「它看到的 socket IP」附加到這個 header 的最右邊。所以 XFF 的格式是一串用逗號分隔的 IP,由左到右代表從最源頭到最近一層的經過順序。
以三層架構為例,假設使用者的 IP 是 203.0.113.50,請求依序經過 Cloudflare(172.64.200.1)和 Nginx:
# Cloudflare 收到請求,socket IP 是使用者的 IP
# Cloudflare 把 203.0.113.50 放進 XFF
X-Forwarded-For: 203.0.113.50
# Nginx 收到請求,socket IP 是 Cloudflare 的 IP
# Nginx 把 172.64.200.1 附加到 XFF 最右邊
X-Forwarded-For: 203.0.113.50, 172.64.200.1
AP 拿到的 XFF 就是 203.0.113.50, 172.64.200.1,最左邊的 203.0.113.50 是使用者的 IP。這聽起來很方便,但 XFF 有一個關鍵的安全問題:使用者可以在發送請求時自己加上 X-Forwarded-For header。如果有人發請求時帶了 X-Forwarded-For: 10.0.0.1,經過 Cloudflare 和 Nginx 之後,AP 收到的會是:
# 使用者自己帶了假的 XFF
X-Forwarded-For: 10.0.0.1, 203.0.113.50, 172.64.200.1
# ^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
# 使用者偽造 Cloudflare 附加 Nginx 附加
直接取 XFF 最左邊的 IP 就會拿到偽造的 10.0.0.1。正確的做法是「從右往左讀,跳過所有信任的代理 IP」。
X-Real-IP:Nginx 幫忙挑好的單一 IP
X-Real-IP 不是標準化的 header,而是 Nginx 生態系常用的慣例。通常在 Nginx 設定裡用 proxy_set_header X-Real-IP $remote_addr; 設定,Nginx 會把它認定的客戶端 IP 放進去。如果 Nginx 有設定 set_real_ip_from 搭配 real_ip_header,這裡的值會是還原過的真實使用者 IP;如果沒設定,就會是 Nginx 直接的 socket IP,也就是 Cloudflare 的 IP。
跟 XFF 相比,X-Real-IP 的好處是只有一個 IP,不需要解析逗號分隔的字串,程式端取用比較簡單。但只有一個值也意味著中間經過的代理鏈資訊就丟失了,沒辦法做更精細的信任判斷。
CF-Connecting-IP:Cloudflare 專屬
CF-Connecting-IP 是 Cloudflare 自己加的 header,值永遠是 Cloudflare 在 TCP 層看到的使用者 IP。跟 XFF 不同,Cloudflare 不會把客戶端自帶的值附加上去,而是直接覆寫,所以這個 header 不會被客戶端偽造(前提是請求確實經過 Cloudflare)。
Cloudflare 另外還有一個 True-Client-IP header,功能一樣,是為了跟 Akamai 的慣例相容而提供的。這個 header 只有 Enterprise 方案才能透過 Managed Transforms 啟用,免費、Pro、Business 方案都不支援。如果沒啟用,Cloudflare 不會設定也不會覆寫這個 header,客戶端反而可以自己偽造,所以非 Enterprise 方案不要依賴 True-Client-IP,直接用 CF-Connecting-IP 就好。
每一層的 header 變化圖解
┌─────────────────────────────────────────────────────────────────┐
│ 使用者瀏覽器 IP: 203.0.113.50 │
│ 發送請求,HTTP header 裡沒有任何代理相關 header │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Cloudflare 節點 IP: 172.64.200.1 │
│ │
│ 收到:socket IP = 203.0.113.50 │
│ 送出: │
│ CF-Connecting-IP: 203.0.113.50 ← Cloudflare 自己寫入 │
│ X-Forwarded-For: 203.0.113.50 ← Cloudflare 附加 │
│ 連線目標:origin server (Nginx) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Nginx IP: 10.0.1.5(或 127.0.0.1) │
│ │
│ 收到:socket IP = 172.64.200.1 (Cloudflare) │
│ 設定 real_ip_header 後:$remote_addr 還原為 203.0.113.50 │
│ 送出: │
│ X-Forwarded-For: 203.0.113.50, 172.64.200.1 ← 附加上一層 IP │
│ X-Real-IP: 203.0.113.50 ← Nginx 設定 │
│ CF-Connecting-IP: 203.0.113.50 ← 原封轉發 │
│ 連線目標:AP Server (localhost:8080) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ AP Server Port: 8080 │
│ │
│ 收到:socket IP = 127.0.0.1 (Nginx) │
│ X-Forwarded-For: 203.0.113.50, 172.64.200.1 │
│ X-Real-IP: 203.0.113.50 │
│ CF-Connecting-IP: 203.0.113.50 │
│ │
│ → 真實使用者 IP 在 header 裡,不在 socket IP │
└─────────────────────────────────────────────────────────────────┘
Nginx 設定
Nginx 預設不會自動還原被代理覆蓋的 IP,需要手動設定兩件事:告訴 Nginx「哪些 IP 是可信任的代理」,以及「從哪個 header 讀取真實 IP」。設定完之後,Nginx 的 $remote_addr 變數就會被還原成使用者的真實 IP,access log 和 proxy_set_header 也會連帶正確。
# /etc/nginx/conf.d/cloudflare-real-ip.conf
# Cloudflare IPv4 範圍(需定期更新)
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 131.0.72.0/22;
# Cloudflare IPv6 範圍
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
# 告訴 Nginx 從 CF-Connecting-IP 讀取真實 IP
real_ip_header CF-Connecting-IP;
# 遞迴解析:如果 XFF 裡有多層代理 IP,逐層跳過信任範圍內的
real_ip_recursive on;
# real_ip_header X-Forwarded-For; # 也可以用 XFF,但 CF-Connecting-IP 更不容易被偽造
Cloudflare 的 IP 範圍可以從 https://www.cloudflare.com/ips/ 取得,建議用 cron job 定期更新。
server {
listen 443 ssl;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080;
# 把還原後的真實 IP 傳給 AP
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 原封轉發 Cloudflare 的 header
proxy_set_header CF-Connecting-IP $http_cf_connecting_ip;
proxy_set_header Host $host;
}
}
$proxy_add_x_forwarded_for 是 Nginx 的內建變數,它會把 $remote_addr 附加到原有的 X-Forwarded-For 後面。如果已經用 real_ip_header 還原過 $remote_addr,這裡附加的就會是真實使用者 IP 而不是 Cloudflare 的 IP。
程式端取得真實 IP
Nginx 設定正確之後,AP 程式端可以從 X-Real-IP 或 CF-Connecting-IP 取得使用者的真實 IP。以下是四種常見語言的範例,每個範例都實作一個 getClientIp 函式,按優先順序嘗試不同的 header。
Node.js(Express)
function getClientIp(req) {
// 優先使用 Cloudflare 提供的 header
const cfIp = req.headers['cf-connecting-ip'];
if (cfIp) return cfIp;
// 其次使用 Nginx 設定的 X-Real-IP
const realIp = req.headers['x-real-ip'];
if (realIp) return realIp;
// 從 X-Forwarded-For 取最左邊的 IP(需搭配信任代理設定)
const xff = req.headers['x-forwarded-for'];
if (xff) return xff.split(',')[0].trim();
// 最後才用 socket IP
return req.socket.remoteAddress;
}
// 使用範例
app.get('/api/info', (req, res) => {
const clientIp = getClientIp(req);
console.log(`Client IP: ${clientIp}`);
res.json({ ip: clientIp });
});
Express 本身有 trust proxy 設定,啟用後 req.ip 會自動解析 XFF。設定方式是 app.set('trust proxy', 'loopback')(信任來自 localhost 的代理),或是用 app.set('trust proxy', '172.64.0.0/13') 指定信任的 CIDR 範圍。啟用之後 req.ip 就會直接回傳解析過的真實 IP,不需要手動解析 header。
Python(Flask)
from flask import Flask, request
app = Flask(__name__)
def get_client_ip():
# 優先使用 Cloudflare 提供的 header
cf_ip = request.headers.get('CF-Connecting-IP')
if cf_ip:
return cf_ip
# 其次使用 Nginx 設定的 X-Real-IP
real_ip = request.headers.get('X-Real-IP')
if real_ip:
return real_ip
# 從 X-Forwarded-For 取最左邊的 IP
xff = request.headers.get('X-Forwarded-For')
if xff:
return xff.split(',')[0].strip()
# 最後才用 socket IP
return request.remote_addr
@app.route('/api/info')
def info():
client_ip = get_client_ip()
return {'ip': client_ip}
Flask 搭配 Werkzeug 的 ProxyFix middleware 可以自動處理 XFF。加上 app.wsgi_app = ProxyFix(app.wsgi_app, x_for=2, x_proto=1) 之後,request.remote_addr 就會自動還原成真實 IP,x_for=2 代表信任 2 層代理(Cloudflare + Nginx)。FastAPI 也有類似的 middleware 設定。
Java(Spring Boot)
@RestController
public class IpController {
@GetMapping("/api/info")
public Map<String, String> info(HttpServletRequest request) {
String clientIp = getClientIp(request);
return Map.of("ip", clientIp);
}
private String getClientIp(HttpServletRequest request) {
// 優先使用 Cloudflare 提供的 header
String cfIp = request.getHeader("CF-Connecting-IP");
if (cfIp != null && !cfIp.isEmpty()) return cfIp;
// 其次使用 Nginx 設定的 X-Real-IP
String realIp = request.getHeader("X-Real-IP");
if (realIp != null && !realIp.isEmpty()) return realIp;
// 從 X-Forwarded-For 取最左邊的 IP
String xff = request.getHeader("X-Forwarded-For");
if (xff != null && !xff.isEmpty()) {
return xff.split(",")[0].trim();
}
// 最後才用 socket IP
return request.getRemoteAddr();
}
}
Spring Boot 內建支援 server.forward-headers-strategy=native(使用 Servlet 容器的 RemoteIpValve)或 framework(使用 Spring 的 ForwardedHeaderFilter)。設定之後 request.getRemoteAddr() 就會自動還原。需要搭配 server.tomcat.remoteip.internal-proxies 設定信任的代理 IP 範圍。
# application.yml
server:
forward-headers-strategy: native
tomcat:
remoteip:
# 信任 Nginx 的本機連線
internal-proxies: "127\\.0\\.0\\.1|::1|10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"
# 指定使用哪個 header 還原 IP
remote-ip-header: "X-Real-IP"
C#(ASP.NET Core)
[ApiController]
[Route("api")]
public class IpController : ControllerBase
{
[HttpGet("info")]
public IActionResult Info()
{
var clientIp = GetClientIp(HttpContext);
return Ok(new { ip = clientIp });
}
private static string GetClientIp(HttpContext context)
{
// 優先使用 Cloudflare 提供的 header
if (context.Request.Headers.TryGetValue("CF-Connecting-IP", out var cfIp))
return cfIp.ToString();
// 其次使用 Nginx 設定的 X-Real-IP
if (context.Request.Headers.TryGetValue("X-Real-IP", out var realIp))
return realIp.ToString();
// 從 X-Forwarded-For 取第一個 IP
if (context.Request.Headers.TryGetValue("X-Forwarded-For", out var xff))
return xff.ToString().Split(',')[0].Trim();
// 最後才用 socket IP
return context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
}
}
ASP.NET Core 內建的 Forwarded Headers Middleware 可以自動處理。在 Program.cs 加上 ForwardedHeadersOptions 設定:
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
// 清除預設的信任範圍,改為明確指定
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
// 信任 Nginx 的本機連線
options.KnownProxies.Add(IPAddress.Parse("127.0.0.1"));
// 信任 Docker 內網(如果 Nginx 跑在容器裡)
options.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("172.16.0.0"), 12));
});
// 要放在 UseAuthorization 之前
app.UseForwardedHeaders();
啟用之後,HttpContext.Connection.RemoteIpAddress 就會自動還原成 XFF 中的真實 IP。
安全注意事項
X-Forwarded-For 可以被偽造
前面提過,使用者可以在請求中自己帶上 X-Forwarded-For header,代理只會往後面附加,不會清掉前面的值。所以直接取 XFF 最左邊的值是不安全的做法。
正確的解法是「從右往左讀,跳過所有已知的信任代理 IP」。假設我們信任 Nginx(127.0.0.1)和 Cloudflare(172.64.0.0/13),對於這個 XFF:
X-Forwarded-For: 10.0.0.1, 203.0.113.50, 172.64.200.1
# ^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
# 偽造的 使用者真實 IP Cloudflare(信任)
從右往左:172.64.200.1 在信任範圍內,跳過;203.0.113.50 不在信任範圍內,這才是真實使用者 IP。而最左邊的 10.0.0.1 是使用者自己偽造的,不應該被採用。
不過在 Cloudflare 架構下,直接用 CF-Connecting-IP 是更簡單也更安全的選擇,因為 Cloudflare 會直接覆寫這個 header,不受客戶端影響。
確認請求確實經過 Cloudflare
CF-Connecting-IP 的安全性建立在「請求確實經過 Cloudflare」這個前提上。如果攻擊者直接連到 origin server(跳過 Cloudflare),他可以自己偽造 CF-Connecting-IP header。防範方式有幾種:

- 防火牆限制:在 origin server 的防火牆只允許 Cloudflare 的 IP 範圍連入 80/443 port,其他 IP 一律拒絕。Cloudflare 的 IP 範圍公布在 cloudflare.com/ips
- Authenticated Origin Pulls:Cloudflare 的 Authenticated Origin Pulls 功能會在連線時使用 Cloudflare 的客戶端憑證,Nginx 端驗證這個憑證就能確認請求來自 Cloudflare
- Cloudflare Tunnel:使用
cloudflared建立 tunnel,origin server 完全不需要對外開放 port,從根本上杜絕直連的可能
沒有 Cloudflare 的架構
如果架構只有 Nginx → AP(沒有 Cloudflare),不會有 CF-Connecting-IP,這時候用 X-Real-IP 就是比較直接的選擇。只要 Nginx 設定了 proxy_set_header X-Real-IP $remote_addr;,而且 Nginx 是唯一的入口(AP 不對外開放 port),X-Real-IP 就是安全的,因為這個值是 Nginx 從 socket 層拿到的,使用者無法偽造。這種單層反向代理的架構設定可以參考 整合 Spring Boot、Nginx 反向代理、Linux Systemd 系統服務 這篇文章。另外,Nginx 反向代理搭配 WordPress 時如果 proxy_set_header 沒設好,可能會碰到 無限重新導向迴圈 的問題,也跟 header 轉發有關。
常見問題
Rate Limit 應該用哪個 IP
如果是在 Nginx 層做 Rate Limit,要先確認 $remote_addr 已經被 real_ip_header 還原成真實 IP。沒還原的話,limit_req_zone $binary_remote_addr 會把所有 Cloudflare 節點的流量算在同一個 IP 上,等於對全部使用者共用一個配額。如果是在 AP 層做 Rate Limit,用 CF-Connecting-IP 或 X-Real-IP 當 key 都可以。
access log 記到的是哪個 IP
Nginx 的 access log 預設用 $remote_addr。如果沒有設定 real_ip_header,log 裡面會全部是 Cloudflare 的 IP,出事的時候查不到是哪個使用者。設定了 real_ip_header 之後,$remote_addr 會被還原,access log 就會記錄使用者的真實 IP。原本的代理 IP 可以用 $realip_remote_addr 變數存取。
Cloudflare IP 範圍多久更新一次
Cloudflare 官方沒有固定的更新頻率,但會在 cloudflare.com/ips 發佈最新的 IP 範圍,也提供 https://www.cloudflare.com/ips-v4 和 https://www.cloudflare.com/ips-v6 兩個純文字端點供腳本抓取。建議每週用 cron job 更新一次 Nginx 的 set_real_ip_from 設定,更新後 nginx -t && nginx -s reload 就好。
#!/bin/bash
# /etc/cron.weekly/update-cloudflare-ips.sh
# 每週自動更新 Cloudflare IP 範圍
CF_CONF="/etc/nginx/conf.d/cloudflare-real-ip.conf"
{
echo "# Cloudflare IP ranges - updated $(date +%Y-%m-%d)"
echo ""
# IPv4
curl -s https://www.cloudflare.com/ips-v4 | while read -r line; do
echo "set_real_ip_from ${line};"
done
echo ""
# IPv6
curl -s https://www.cloudflare.com/ips-v6 | while read -r line; do
echo "set_real_ip_from ${line};"
done
echo ""
echo "real_ip_header CF-Connecting-IP;"
} > "${CF_CONF}"
# 測試設定後重載
nginx -t && nginx -s reload
IPv6 環境下有什麼不同
行為完全一樣,只是 IP 格式不同。CF-Connecting-IP 和 X-Forwarded-For 都會正確攜帶 IPv6 位址。程式端解析時要注意 IPv6 位址可能包含冒號(如 2001:db8::1),在 XFF 裡用逗號分隔不會有歧義,但如果要做正規表達式比對或存進資料庫,要確認欄位長度夠放 IPv6 的完整格式(最長 45 字元,包含 ::ffff:192.0.2.1 這種 IPv4-mapped 格式)。
使用者用 VPN 連線時拿到的 IP 是什麼
如果使用者透過 VPN 連線,Cloudflare 在 TCP 層看到的就是 VPN 出口節點的 IP,CF-Connecting-IP 記錄的也會是 VPN 的 IP 而非使用者的原始 IP。這不是 Cloudflare 特有的限制,而是所有 HTTP 層代理的共同行為——VPN 在更底層就已經把來源 IP 換掉了,任何位於 VPN 之後的節點都無法看到 VPN 背後的真實 IP。同樣的道理也適用於企業內部的 proxy、Tor 出口節點等場景。