Java獲取客戶端真實(shí)IP地址經(jīng)典寫法詳解
在Web開發(fā)中,獲取客戶端的真實(shí)IP地址是一個(gè)常見需求。由于客戶端可能經(jīng)過代理、負(fù)載均衡或CDN,request.getRemoteAddr() 往往只能拿到代理服務(wù)器的IP。因此,需要從特定的HTTP頭(如 X-Forwarded-For、Proxy-Client-IP、WL-Proxy-Client-IP)中解析出原始IP。
下面分別給出經(jīng)典寫法(基于工具類/條件判斷)和Lambda寫法(基于函數(shù)式接口 + 流式處理)的實(shí)現(xiàn),并進(jìn)行對(duì)比。
經(jīng)典寫法(傳統(tǒng)工具類)
使用 if-else 鏈和循環(huán)逐層解析,邏輯清晰,易于理解和調(diào)試。
import javax.servlet.http.HttpServletRequest;
public class IpUtils {
/**
* 獲取客戶端真實(shí)IP地址
* @param request HttpServletRequest
* @return 真實(shí)IP,如果無(wú)法獲取則返回 "unknown"
*/
public static String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 對(duì)于通過多級(jí)代理的情況,取第一個(gè)非 unknown 的IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}Lambda寫法(Java 8+)
利用 Stream 和 Optional 對(duì)頭名稱列表進(jìn)行鏈?zhǔn)教幚?,代碼更緊湊、函數(shù)式風(fēng)格更明顯。
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Optional;
public class IpUtilsLambda {
private static final String[] HEADERS_TO_TRY = {
"X-Forwarded-For",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
/**
* 獲取客戶端真實(shí)IP地址(Lambda風(fēng)格)
* @param request HttpServletRequest
* @return 真實(shí)IP,默認(rèn)返回 request.getRemoteAddr()
*/
public static String getClientIp(HttpServletRequest request) {
String ip = Arrays.stream(HEADERS_TO_TRY)
.map(request::getHeader)
.filter(header -> header != null && !header.isEmpty() && !"unknown".equalsIgnoreCase(header))
.findFirst()
.orElseGet(request::getRemoteAddr);
// 處理多級(jí)代理情況
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}或者更進(jìn)一步,將分割邏輯也融入流中:
public static String getClientIpAdvanced(HttpServletRequest request) {
return Arrays.stream(HEADERS_TO_TRY)
.map(request::getHeader)
.filter(header -> header != null && !header.isEmpty() && !"unknown".equalsIgnoreCase(header))
.map(header -> header.contains(",") ? header.split(",")[0].trim() : header)
.findFirst()
.orElseGet(request::getRemoteAddr);
}兩種寫法對(duì)比
| 對(duì)比維度 | 經(jīng)典寫法 | Lambda寫法 |
|---|---|---|
| 可讀性 | 直觀,每個(gè)條件分支清晰可見,適合所有Java開發(fā)者 | 代碼緊湊,但對(duì)不熟悉函數(shù)式編程的開發(fā)者有一定閱讀門檻 |
| 代碼行數(shù) | 較多(約20行) | 較少(約10行) |
| 擴(kuò)展性 | 添加新頭需要增加一個(gè) if 塊 | 只需在 HEADERS_TO_TRY 數(shù)組中增加一個(gè)元素 |
| 性能 | 幾乎無(wú)差異,都是 O(n) 遍歷,且只在請(qǐng)求生命周期內(nèi)執(zhí)行一次 | 同樣 O(n),但 Stream 會(huì)引入少量額外開銷(微乎其微) |
| 調(diào)試便利性 | 可在任意 if 處打斷點(diǎn),逐行跟蹤 | 流內(nèi)調(diào)試相對(duì)困難,需借助 peek() 或 IDE 的流調(diào)試插件 |
| 錯(cuò)誤處理 | 可針對(duì)每個(gè)頭單獨(dú)處理異?;蛉罩?/td> | 需要額外在 map 或 filter 中處理,不夠直觀 |
| 團(tuán)隊(duì)接受度 | 高,任何Java程序員都能立即理解 | 依賴于團(tuán)隊(duì)對(duì)函數(shù)式編程的熟悉程度 |
注意事項(xiàng)與最佳實(shí)踐
- 代理信任:
X-Forwarded-For頭可以被偽造,如果應(yīng)用直接暴露在公網(wǎng),不應(yīng)完全信任該頭。通常需要在反向代理(如Nginx)層面配置proxy_set_header X-Forwarded-For $remote_addr,并限制內(nèi)部網(wǎng)絡(luò)才能傳遞該頭。 - IP格式:IPv4和IPv6均可能出現(xiàn)在頭中,無(wú)需額外處理,直接取字符串即可。
- 多級(jí)代理:
X-Forwarded-For的值格式為client, proxy1, proxy2,通常取第一個(gè)(最左側(cè))為真實(shí)客戶端IP。 - 空值處理:許多代理會(huì)返回
unknown字符串,需要過濾掉。 - 性能考慮:獲取IP的操作發(fā)生在每個(gè)請(qǐng)求中,但遍歷幾個(gè)頭字符串的性能開銷可以忽略不計(jì)。
- Spring Boot集成:如果使用Spring Boot,也可以直接使用內(nèi)置工具類
RequestUtils.getRemoteAddress()或ServletWebRequest.getHeaderValues(),但原理與上述相同。
方法補(bǔ)充
下面對(duì)比了兩種Java獲取客戶端IP的實(shí)現(xiàn)方式。經(jīng)典寫法使用for循環(huán)遍歷IP頭信息數(shù)組,逐個(gè)檢查并返回第一個(gè)有效IP;Lambda寫法則利用Stream API,通過鏈?zhǔn)讲僮魍瓿上嗤倪壿?,代碼更簡(jiǎn)潔。兩種方法都考慮了多種代理頭信息(X-Forwarded-For等)和IP格式處理(分割、去空、trim),最終回退到request.getRemoteAddr()。Lambda寫法展現(xiàn)了函數(shù)式編程的優(yōu)勢(shì),但兩者在功能上完全等效。
經(jīng)典寫法
@Autowired
HttpServletRequest request;
String[] IP_HEADERS = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED", "HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR"};
private String getClientIpClassic()
{
log.info("##### Classic");
for (String header : IP_HEADERS)
{
String ip = request.getHeader(header);
if (StringUtils.isNotBlank(ip) && !StringUtils.equalsIgnoreCase("unknown", ip))
{
log.info("##### ip header: {}", header);
String[] ips = ip.split(",");
return ips[0].trim();
}
}
log.info("##### request.getRemoteAddr");
return request.getRemoteAddr();
}
Lambda寫法
private String getClientIpLambda()
{
log.info("##### Lambda");
return Arrays.stream(IP_HEADERS)
.peek(log::info)
.map(request::getHeader)
.filter(ips -> StringUtils.isNotBlank(ips) && !"unknown".equalsIgnoreCase(ips))
.flatMap(ips -> Arrays.stream(ips.split(",")))
.filter(StringUtils::isNotBlank)
.map(String::trim)
.findFirst()
.orElse(request.getRemoteAddr());
}
總結(jié)
- 經(jīng)典寫法:適合大多數(shù)項(xiàng)目,尤其是維護(hù)性優(yōu)先、團(tuán)隊(duì)成員技術(shù)水平不一的場(chǎng)景。它直觀、易于調(diào)試和擴(kuò)展,沒有額外的學(xué)習(xí)成本。
- Lambda寫法:適合追求代碼簡(jiǎn)潔、熟悉函數(shù)式編程、且頭名稱固定不變的場(chǎng)景。它減少了樣板代碼,讓意圖更聚焦于“從一系列候選頭中找出第一個(gè)非空的有效值”。
實(shí)際項(xiàng)目中,兩種寫法均可正確工作。如果項(xiàng)目已經(jīng)使用 Java 8+ 并普遍采用 Stream API,Lambda寫法可以提升代碼的表達(dá)力;否則,經(jīng)典寫法更加穩(wěn)妥。
到此這篇關(guān)于Java獲取客戶端真實(shí)IP地址經(jīng)典寫法詳解的文章就介紹到這了,更多相關(guān)Java獲取客戶端IP地址內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java并發(fā)編程之詳解ConcurrentHashMap類
在之前的文章中已經(jīng)為大家介紹了java并發(fā)編程的工具:BlockingQueue接口、ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue、BlockingDeque接口,本文為系列文章第八篇.需要的朋友可以參考下2021-06-06
Java Web開發(fā)項(xiàng)目中中文亂碼解決方法匯總
這篇文章主要為大家詳細(xì)匯總了Java Web開發(fā)項(xiàng)目中中文亂碼的解決方法,分析了5種Java Web中文亂碼情況,感興趣的小伙伴們可以參考一下2016-05-05
Java?ConcurrentHashMap實(shí)現(xiàn)線程安全的代碼示例
眾所周知ConcurrentHashMap是HashMap的多線程版本,HashMap?在并發(fā)操作時(shí)會(huì)有各種問題,而這些問題,只要使用ConcurrentHashMap就可以完美解決了,本文將給詳細(xì)介紹ConcurrentHashMap是如何保證線程安全的2023-05-05
Java實(shí)現(xiàn)單鏈表SingleLinkedList增刪改查及反轉(zhuǎn) 逆序等
單鏈表是鏈表的其中一種基本結(jié)構(gòu)。一個(gè)最簡(jiǎn)單的結(jié)點(diǎn)結(jié)構(gòu)如圖所示,它是構(gòu)成單鏈表的基本結(jié)點(diǎn)結(jié)構(gòu)。在結(jié)點(diǎn)中數(shù)據(jù)域用來(lái)存儲(chǔ)數(shù)據(jù)元素,指針域用于指向下一個(gè)具有相同結(jié)構(gòu)的結(jié)點(diǎn)。 因?yàn)橹挥幸粋€(gè)指針結(jié)點(diǎn),稱為單鏈表2021-10-10
詳解Java多線程編程中CountDownLatch阻塞線程的方法
在Java中和ReadWriteLock.ReadLock一樣,CountDownLatch的本質(zhì)也是一個(gè)"共享鎖",這里我們就來(lái)詳解Java多線程編程中CountDownLatch阻塞線程的方法:2016-07-07
Java的Cglib動(dòng)態(tài)代理實(shí)現(xiàn)方式詳解
這篇文章主要介紹了Java的Cglib動(dòng)態(tài)代理實(shí)現(xiàn)方式詳解,CGLIB是強(qiáng)大的、高性能的代碼生成庫(kù),被廣泛應(yīng)用于AOP框架,它底層使用ASM來(lái)操作字節(jié)碼生成新的類,為對(duì)象引入間接級(jí)別,以控制對(duì)象的訪問,需要的朋友可以參考下2023-11-11
SpringBoot_Cache自定義使用SimpleCacheManager方式
這篇文章主要介紹了SpringBoot_Cache自定義使用SimpleCacheManager方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07
Spring MVC入門_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Spring MVC入門,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來(lái)看看吧2017-08-08

