在分佈式架構與雲原生環境下,客戶端請求往往經過 Nginx、網關、CDN、負載均衡器(SLB/ALB)多層代理才到達應用服務器。此時 HttpServletRequest.getRemoteAddr() 只能拿到直接上游的 IP(如 Nginx IP),而非真實用戶 IP。本文提供生產級的 IP 獲取工具方法,涵蓋標準頭部解析、可信代理鏈驗證、IPv6 兼容與安全防範。
| 頭部名稱 | 來源/標準 | 說明 |
|---|---|---|
X-Forwarded-For (XFF) | 事實標準 (RFC 7239) | 格式:client, proxy1, proxy2,最左側為最原始客戶端 IP。可能被偽造。
|
X-Real-IP | Nginx/HAProxy 慣例 | 單一 IP 值,通常由邊緣代理設置,相對可信。 |
Forwarded | RFC 7239 標準 | 結構化格式:for=192.0.2.60;proto=https;by=203.0.113.43,支持 IPv6 與多代理鏈。
|
X-Forwarded-Proto | 慣例 | 原始協議(http/https),用於判斷是否需重定向。 |
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.List;
public final class IpUtil {
private static final List<String> IP_HEADERS = Arrays.asList(
"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP",
"HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR", "Forwarded"
);
/** 可信代理 IP 白名單(生產環境必須配置,防止頭部偽造) */
private static final List<String> TRUSTED_PROXIES = Arrays.asList(
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" // 內網 CIDR 示例
);
private IpUtil() {}
/**
* 獲取客戶端真實 IP
* @param request HttpServletRequest
* @return 真實 IP,獲取失敗返回 "unknown"
*/
public static String getRealIp(HttpServletRequest request) {
if (request == null) return "unknown";
// 1. 優先解析 RFC 7239 Forwarded 標準頭
String forwarded = request.getHeader("Forwarded");
if (forwarded != null) {
String ip = parseForwardedHeader(forwarded);
if (isValidIp(ip)) return ip;
}
// 2. 遍歷常見代理頭
for (String header : IP_HEADERS) {
String value = request.getHeader(header);
if (value != null && !value.isEmpty() && !"unknown".equalsIgnoreCase(value)) {
// X-Forwarded-For 可能包含多個 IP,取最左側非私有/非代理 IP
String[] ips = value.split("\\s*,\\s*");
for (String ip : ips) {
if (isValidIp(ip) && !isPrivateOrProxyIp(ip)) {
return ip;
}
}
// 若全是內網/代理 IP,返回第一個有效 IP
for (String ip : ips) {
if (isValidIp(ip)) return ip;
}
}
}
// 3. 回退到 RemoteAddr
String remoteAddr = request.getRemoteAddr();
return isValidIp(remoteAddr) ? remoteAddr : "unknown";
}
/** 解析 Forwarded 頭:for=IP[, for=IP...] */
private static String parseForwardedHeader(String header) {
// 簡易解析,生產建議用成熟庫(如 Spring ForwardedHeaderFilter)
String[] parts = header.split(";\\s*");
for (String part : parts) {
if (part.startsWith("for=")) {
String val = part.substring(4).trim();
// 去除引號、端口、IPv6 方括號
val = val.replaceAll("^\"|\"$", "").split(":")[0].replaceAll("^\\[|\\]$", "");
return val;
}
}
return null;
}
/** 基礎 IP 格式校驗(IPv4/IPv6) */
private static boolean isValidIp(String ip) {
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) return false;
// 簡易正則,嚴格場景建議用 InetAddress.getByName() 嘗試解析
return ip.matches("^(\\d{1,3}\\.){3}\\d{1,3}$") || ip.matches("^[0-9a-fA-F:]+$");
}
/** 判斷是否為私有 IP 或 受信代理 IP(需結合 CIDR 庫精確匹配) */
private static boolean isPrivateOrProxyIp(String ip) {
// 此處為示例邏輯,實際項目請引入 ipmatcher 或 Guava InetAddresses 進行 CIDR 匹配
return ip.startsWith("10.") || ip.startsWith("192.168.") ||
ip.matches("^172\\.(1[6-9]|2\\d|3[0-1])\\..*") ||
ip.equals("127.0.0.1") || ip.equals("0:0:0:0:0:0:0:1");
}
}
ForwardedHeaderFilter 自動解析@Bean
public ForwardedHeaderFilter forwardedHeaderFilter() {
return new ForwardedHeaderFilter(); // 自動將 Forwarded/X-Forwarded-* 映射到 request.getRemoteAddr() 等
}
配合 server.forward-headers-strategy=native(Spring Boot 2.2+)或 server.use-forward-headers=true(舊版),容器層面即可完成 IP 重寫。
X-Original-Client-IP)傳遞給下游微服務。proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Forwarded "for=$remote_addr;proto=$scheme;by=$server_addr";
X-Forwarded-For 等頭部。RemoteAddr、X-Forwarded-For、X-Real-IP、Forwarded 所有頭部,便於審計追溯。[] 方括號格式、Zone ID(如 fe80::1%eth0)解析正確。| 誤區 | 後果 | 正確做法 |
|---|---|---|
| 直接取 XFF 第一個 IP | 惡意用戶偽造頭部繞過限流/封禁 | 結合可信代理白名單,從右向左找第一個非代理 IP |
| 忽略 Forwarded 標準頭 | 無法支持 RFC 7239 合規代理鏈 | 優先解析 Forwarded,兼容舊有 XFF |
| 將 127.0.0.1 視為真實 IP | 本地測試/內部調用污染統計 | 過濾回環/私有地址段 |
| 未處理 IPv6 地址 | 雙棧環境下獲取失敗 | 使用 InetAddress 統一解析,存儲標準格式 |
掌握真實 IP 獲取的核心邏輯與安全邊界,是構建可靠訪問控制、風控分析、審計日誌系統的基石。建議在項目公共模組封裝 IpUtil,並納入代碼審查清單,確保全鏈路一致性與安全性。