分享程式代碼相關筆記
目前文章總數:203 篇
最後更新:2025年 10月 25日
以下是此次說明用到的環境架構
| 角色 | 說明 | |
|---|---|---|
| 1. 用戶端 | : | 訪問的用戶 IP : 192.168.242.76 |
| 2. 伺服器主機 | : | 提供用戶訪問的 Web 機器,機器 IP:192.168.51.93 |
| 3. Nginx (伺服器內) | : | 安裝於伺服器主機,並且與容器 8888 Port 綁定,對外反向代理 8889 |
| 反向代理後完整路徑: 192.168.51.93:8889 | ||
| 4. 容器 (伺服器內) | : | 安裝於伺服器主機,Docker 容器,使用 8888 Port |
| 用戶直連完整路徑: 192.168.51.93:8888 |
並且運行的 Web 應用程式安裝在容器上,並且開放了以下端口:
※取得 IP 使用的屬性 : HttpContext.Connection.RemoteIpAddress
/// <summary>
/// 當前訪問者(用戶) 後端紀錄的 IP
/// </summary>
[HttpGet]
public IActionResult CallAPI()
{
var remoteIp = HttpContext.Connection.RemoteIpAddress;
// 方便 Demo 說明轉換成純 IPv4
var ipv4 = remoteIp?.MapToIPv4();
var result = new
{
RemoteIp = ipv4.ToString(),
Scheme = HttpContext.Request.Scheme,
Host = HttpContext.Request.Host.Value
};
return Ok(result);
}
並且對應的呼叫 API 路由如下:
http://{Domain}//Home//CallAPI
我們透過本機電腦訪問容器位址,中間過程沒有任何 反向代理 ,視為直連
輸入以下 URL 網址列,可以看到 API 反饋的 IP 為 192.168.242.76 正確的獲得了訪問者的 IP
http://192.168.51.93:8888/Home/CallAPI
我們透過本機電腦訪問 Nginx 對外位址,有 經過反向代理
輸入以下 URL 網址列,可以看到 API 反饋的 IP 為 192.168.51.93 這是 伺服器IP 而非用戶IP
http://192.168.51.93:8889/Home/CallAPI
這實際上與轉發標頭 X-Forwarded-For 有關,以下是不同訪問模式下的轉發層級:
| 用戶訪問模式 | 經過路徑 | 層數(自己算1) | |
|---|---|---|---|
| 直連 | : | 用戶 => 容器 | 1 |
| 經過代理 | : | 用戶 => Nginx -> 容器 | 2 |
直連時,HttpContext.Connection.RemoteIpAddress 會顯示用戶的真實 IP
經過代理時,HttpContext.Connection.RemoteIpAddress 只能看到最近一層代理的 IP (Nginx IP),而不是用戶的真實 IP
打開本篇範例代碼後,架構基本分成以下:
※ 調整前專案 :NotUseForwardedHeadersExample
※ 調整後專案 :UseForwardedHeadersExample
| 1. 初始化配置 | : | 初始化配置 ForwardedHeadersOptions ,使其可觀察到真實 IP |
| 2. 控制器 | : | 取得 HttpContext 組態資料 |
| 3. Web 檢視器 | : | 響應 API 的結果到頁面上,便於觀察 |
| 4. Dockerfile | : | 容器化部署會用到的 Dockerfile |
在初始化配置中設定以下,添加 ForwardedHeaders,以下是解決此問題所需參數與說明:
| 1. ForwardedHeaders | 處理 X-Forwarded-For (客戶端真實 IP) |
| 處理 X-Forwarded-Proto (原始協定 http/https) | |
| 2. ForwardLimit | 限制最多處理幾層代理 |
using Microsoft.AspNetCore.HttpOverrides;
try
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// 1. 添加 ForwardedHeaders
var forwardedHeadersOptions = new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto,
ForwardLimit = 5
};
app.UseForwardedHeaders(forwardedHeadersOptions);
//... 略
app.Run();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
至此設定完成後,可以直接進到 第四部分:DEMO 成果 ,第三部份後半段為安全性、建議說明
以下是說明 ForwardedHeaders 的其他參數,基本上會用到的只有 3 種參數配置,其餘多半在生產環境上可以不用配置
| 參數 | 用途 | 生產環境是否必須 | 說明 |
|---|---|---|---|
| 1. ForwardedHeaders | 指定要處理哪些轉發標頭(Header) | - | 依照需求設定 |
| 2. ForwardLimit | 限制最多處理幾層代理 | √ | 合理配置數值,設為 Null 會為不限制,有風險 |
| 3. KnownProxies | 信任的代理 IP 清單 | √ | 依照生產環境配置,信任已知的負載平衡、反向代理IP |
| 4. KnownNetworks | 信任的網路區段 | √ | 依照生產環境配置網路區段,信任內網區段 |
| 5. AllowedHosts | 允許的主機名 | X | HostFiltering 中介軟體專門處理主機名驗證,並免同時使用 |
| 6. RequireHeaderSymmetry | 標頭對稱性檢查 | X | 預設 False ,若需要強制所有代理層標頭一致才需要 |
| 7. ForwardedForHeaderName | 自訂標頭名稱 | X | 遵循標頭規範名稱,請勿自訂名稱,通常用來做為測試,或客製化使用 |
| 8. OriginalForHeaderName | 用於保存原始標頭值 | X | 不用,中間的代理紀錄更為合適 |
| 9. OriginalProtoHeaderName | 用於保存原始標頭值 | X | 不用,中間的代理紀錄更為合適 |
以下是預設代碼參考:
var forwardedHeadersOptions = new ForwardedHeadersOptions
{
// 處理所有轉發標頭
ForwardedHeaders = ForwardedHeaders.XForwardedFor
| ForwardedHeaders.XForwardedProto
| ForwardedHeaders.XForwardedHost,
// 合理的轉發
ForwardLimit = 5,
};
// 信任特定的代理伺服器
forwardedHeadersOptions.KnownProxies.Add(IPAddress.Parse("10.0.0.100"));
// 或信任整個內網
forwardedHeadersOptions.KnownNetworks.Add(
new IPNetwork(IPAddress.Parse("10.0.0.0"), 8)
);
forwardedHeadersOptions.KnownNetworks.Add(
new IPNetwork(IPAddress.Parse("172.16.0.0"), 12)
);
forwardedHeadersOptions.KnownNetworks.Add(
new IPNetwork(IPAddress.Parse("192.168.0.0"), 16)
);
延續 第三部分 Step 2. 將調整後的代碼,重新部署代碼到環境
直連的方式
仍可以正常取得 IP - 符合預期
經過反向代理 的方式
也可以正常取得 IP - 在有反向代理的情況下,仍能更有機會取得真實的用戶IP,解決了無法取得用戶真實 IP 的問題
※強調:始終不可以相信前端用戶傳給伺服器的任何東西,包含 IP,這是可以偽造的,但透過此方法是讓我們能更有高精確 + 安全性的取得用戶IP
問題 : 在這邊點出個問題,有必要使用 ForwardedHeadersOptions 來取得真實 IP 嗎? 即使不用此方法,不也可以取得
解答 : 是的,但一般代碼可能實作如下(Web API):
[HttpGet]
public IActionResult GetClientIP()
{
char[] Separator = { ',' };
var httpRequest = HttpContext.Request;
var clientIp = HttpContext.Connection.RemoteIpAddress?.ToString() ?? string.Empty;
if (HttpContext.Request.Headers.Count == 0)
{
Console.WriteLine("沒有請求 Header 視為異常,不解析 IP");
return BadRequest();
}
// 1. 檢查標頭 X-Forwarded-For - 自己實作
if (Request.Headers.TryGetValue("X-Forwarded-For", out var forwardedIps))
{
var ips = forwardedIps.ToString().Split(Separator, StringSplitOptions.RemoveEmptyEntries);
clientIp = ips[0];// 2. 直接取第 1 個 IP,缺少驗證任何來源,任何偽造都無方防護
}
// 3. 自己刻信任的網段、白名單 IP、黑名單 IP ... 略
var result = new
{
RemoteIp = clientIp,
};
return Ok(result);
}
開發團隊可以自己解析 Headers 內的東西,然後取得 IP,更完整實作的會檢查此 IP 是否符合公司內部信任域名 … 等
回到原本的問題:微軟已經有現成的 ForwardedHeadersOptions 並實作 KnownNetworks、KnownProxies 更安全的封裝方法,現代開發者應該避免自己花時間重刻方法。
微軟給定的 ForwardLimit 預設值為 1 ,但實務上環境的架構很多種可能:
用戶 => CloudFlare => LoadBalnce => Nginx => 容器Nginx => 容器
用戶 => Amazon Cloud => CloudFlare => LoadBalnce => Nginx => 容器
...
...
...
太多種可能
以下是可以使用的常見架構下的參考值:
| 架構類型 | 架構圖示 | ForwardLimit | KnownNetworks 建議 |
|---|---|---|---|
| 本地開發 | 客戶端 → 應用 | 1 | 不需要 |
| 單層反向代理 | 客戶端 → Nginx → 應用 | 1-2 | 172.16.0.0/12, 192.168.0.0/16 |
| Docker Compose | 客戶端 → Nginx → Docker 容器 | 2 | 172.16.0.0/12 |
| Kubernetes 基礎 | 客戶端 → Ingress → Service → Pod | 2-3 | 10.0.0.0/8 |
| 雲端單層 | 客戶端 → AWS ALB → 應用 | 1 | AWS VPC CIDR |
| CDN + 雲端 | 客戶端 → Cloudflare → ALB → 應用 | 2 | VPC CIDR |
| 完整企業架構 | 客戶端 → CDN → WAF → LB → Nginx → 應用 | 4-5 | 內網全段 |
| Service Mesh | 客戶端 → Ingress → Envoy → Envoy → Pod | 3-5 | Kubernetes Pod CIDR |
因此多數的情況下,ForwardLimit 設定 5 已可解決常見的部署架構。
X-Forwared-For 標頭是由客戶端提供給後端的,因此是可以輕易偽造,透過以下 Windows Batch 指令可以輕鬆偽造
curl -H "X-Forwarded-For: 8.8.8.8, 1.1.1.1" https://your-api.com/api/TestAPI
所以需保持一個原則 不可能 100% 取得客戶端真實IP
雖然最終部署到生產環境,但開發的過程是循序漸進,不可能為每個環境都配置信任的域名、Limit
本機開發 → DEV → QAT → STG → PRO
其次每個企業組織架構不同,當開發並 不能管理生產環境 時,往往面臨的將會是 開發團隊不清楚網路架構,因為是由另一個部門處理公司的對外網路與機器管理
因此 不要過度追求完美,符合實際部署需求,實用至上!