SpringBoot中獲取真實客戶端IP的終極方案
引言:為什么你的IP獲取方式可能是錯的?
在日常開發(fā)中,獲取客戶端IP看似簡單,實則暗藏玄機(jī)。很多開發(fā)者直接使用request.getRemoteAddr(),結(jié)果在生產(chǎn)環(huán)境中發(fā)現(xiàn)獲取到的都是負(fù)載均衡器的IP,而非真實用戶IP。更糟糕的是,有些方案存在安全漏洞,可能被惡意用戶偽造IP地址。
今天,我將徹底揭秘Spring Boot中獲取真實客戶端IP的正確姿勢,讓你避開所有坑!
一、理解IP傳遞的底層原理
1.1 為什么需要特殊處理
在現(xiàn)代Web架構(gòu)中,請求往往要經(jīng)過多個中間件:
用戶 → CDN → 負(fù)載均衡器 → 網(wǎng)關(guān) → 應(yīng)用服務(wù)器
每個環(huán)節(jié)都會修改請求信息,導(dǎo)致簡單的getRemoteAddr()失效。
1.2 關(guān)鍵HTTP頭字段解析
| 頭字段 | 含義 | 示例 | 可信度 |
|---|---|---|---|
| X-Forwarded-For | 代理鏈IP序列 | 1.2.3.4, 5.6.7.8 | ???? |
| X-Real-IP | 最后一個代理IP | 1.2.3.4 | ??? |
| Proxy-Client-IP | Apache代理IP | 1.2.3.4 | ?? |
| WL-Proxy-Client-IP | WebLogic代理IP | 1.2.3.4 | ?? |
1.3 X-Forwarded-For的深度解析
這才是重點! X-Forwarded-For是獲取真實IP的關(guān)鍵,但很多人用錯了!
X-Forwarded-For: 客戶端真實IP, 代理服務(wù)器1IP, 代理服務(wù)器2IP, ...
重要規(guī)則:
- 最左邊的IP是原始客戶端IP
- 后續(xù)IP是經(jīng)過的代理服務(wù)器IP
- 多個IP用逗號分隔
實際場景示例:
// 場景1:直接訪問(無代理) X-Forwarded-For: null // 場景2:經(jīng)過CDN X-Forwarded-For: 123.45.67.89 // 場景3:CDN + Nginx負(fù)載均衡 X-Forwarded-For: 123.45.67.89, 10.0.1.100 // 場景4:復(fù)雜代理鏈 X-Forwarded-For: 123.45.67.89, 203.0.113.195, 198.51.100.10
二、終極解決方案:安全可靠的IP工具類
下面這個工具類經(jīng)過生產(chǎn)環(huán)境千錘百煉,直接復(fù)制使用即可!
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* IP工具類 - 獲取真實客戶端IP地址
* 支持多級代理、防止IP偽造、安全可靠
*/
public class IpUtils {
private static final String UNKNOWN = "unknown";
private static final String LOCALHOST_IP = "127.0.0.1";
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
private static final String SEPARATOR = ",";
// 內(nèi)網(wǎng)IP段(用于識別代理服務(wù)器)
private static final Set<String> INTERNAL_IP_SEGMENTS = new HashSet<>(Arrays.asList(
"10.", "192.168.", "172.16.", "172.17.", "172.18.", "172.19.",
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.",
"172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31."
));
/**
* 獲取真實客戶端IP(推薦使用)
* 安全可靠,防止偽造,支持多級代理
*/
public static String getClientRealIp(HttpServletRequest request) {
// 1. 優(yōu)先檢查X-Forwarded-For(處理多級代理)
String ip = parseXForwardedFor(request.getHeader("X-Forwarded-For"));
if (isValidPublicIp(ip)) {
return ip;
}
// 2. 檢查其他代理頭
ip = getIpFromHeaders(request);
if (isValidPublicIp(ip)) {
return ip;
}
// 3. 最后使用RemoteAddr
ip = request.getRemoteAddr();
return LOCALHOST_IPV6.equals(ip) ? LOCALHOST_IP : ip;
}
/**
* 解析X-Forwarded-For頭(核心邏輯)
*/
private static String parseXForwardedFor(String xffHeader) {
if (xffHeader == null || xffHeader.trim().isEmpty()) {
return null;
}
String[] ips = xffHeader.split(SEPARATOR);
// 從右向左查找第一個公網(wǎng)IP(更安全)
for (int i = ips.length - 1; i >= 0; i--) {
String ip = ips[i].trim();
if (isValidIp(ip) && !isInternalIp(ip)) {
return ip;
}
}
// 如果沒有公網(wǎng)IP,返回第一個有效IP
for (String ip : ips) {
String trimmedIp = ip.trim();
if (isValidIp(trimmedIp)) {
return trimmedIp;
}
}
return null;
}
/**
* 從其他頭字段獲取IP
*/
private static String getIpFromHeaders(HttpServletRequest request) {
String[] headers = {
"X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP",
"HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"
};
for (String header : headers) {
String ip = request.getHeader(header);
if (isValidIp(ip)) {
return ip;
}
}
return null;
}
/**
* 驗證IP是否有效
*/
private static boolean isValidIp(String ip) {
return ip != null &&
!ip.isEmpty() &&
!UNKNOWN.equalsIgnoreCase(ip) &&
isValidIpAddress(ip);
}
/**
* 驗證是否為公網(wǎng)IP
*/
private static boolean isValidPublicIp(String ip) {
return isValidIp(ip) && !isInternalIp(ip) && !isLocalhost(ip);
}
/**
* 檢查是否為內(nèi)網(wǎng)IP
*/
private static boolean isInternalIp(String ip) {
if (ip == null) return false;
return INTERNAL_IP_SEGMENTS.stream().anyMatch(ip::startsWith);
}
/**
* 檢查是否為本地地址
*/
private static boolean isLocalhost(String ip) {
return LOCALHOST_IP.equals(ip) || LOCALHOST_IPV6.equals(ip);
}
/**
* 驗證IP地址格式
*/
public static boolean isValidIpAddress(String ip) {
if (ip == null || ip.isEmpty()) return false;
// IPv4驗證
String ipv4Pattern = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$";
if (ip.matches(ipv4Pattern)) return true;
// IPv6簡化驗證
if (ip.contains(":")) return true;
return false;
}
}
三、Spring Boot配置:讓服務(wù)器認(rèn)識代理
3.1 Tomcat代理配置
@Configuration
public class TomcatProxyConfig {
/**
* 配置Tomcat識別代理頭
*/
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatProxyCustomizer() {
return factory -> factory.addConnectorCustomizers(connector -> {
connector.setProperty("relaxedQueryChars", "|{}[]");
connector.setProperty("relaxedPathChars", "|{}[]");
connector.setProperty("remoteIpHeader", "x-forwarded-for");
connector.setProperty("protocolHeader", "x-forwarded-proto");
// 信任的內(nèi)網(wǎng)代理(根據(jù)實際情況調(diào)整)
connector.setProperty("internalProxies",
"192\\.168\\.\\d{1,3}\\.\\d{1,3}|10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|172\\.(1[6-9]|2[0-9]|3[0-1])\\.\\d{1,3}\\.\\d{1,3}");
});
}
}
3.2 應(yīng)用配置文件
# application.yml
server:
tomcat:
remoteip:
remote-ip-header: x-forwarded-for
protocol-header: x-forwarded-proto
internal-proxies: |
192\.168\.\d{1,3}\.\d{1,3}|10\.\d{1,3}\.\d{1,3}\.\d{1,3}|
172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}
spring:
mvc:
log-request-details: true
四、高級功能:IP攔截與安全防護(hù)
4.1 IP攔截器(自動記錄)
@Component
public class IpLoggingInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(IpLoggingInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String clientIp = IpUtils.getClientRealIp(request);
request.setAttribute("clientRealIp", clientIp);
// 記錄訪問日志
logger.info("客戶端訪問: IP={}, URI={}, User-Agent={}",
clientIp,
request.getRequestURI(),
request.getHeader("User-Agent"));
return true;
}
}
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private IpLoggingInterceptor ipLoggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipLoggingInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/health", "/metrics");
}
}
4.2 IP安全過濾器(防刷/黑名單)
@Component
@Order(1)
public class IpSecurityFilter implements Filter {
// IP黑名單(可從數(shù)據(jù)庫或配置中心加載)
private final Set<String> blacklistedIps = ConcurrentHashMap.newKeySet();
// IP訪問頻率限制(簡單的內(nèi)存實現(xiàn),生產(chǎn)環(huán)境建議用Redis)
private final Map<String, RateLimitInfo> rateLimitMap = new ConcurrentHashMap<>();
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String clientIp = IpUtils.getClientRealIp(httpRequest);
// 1. 黑名單檢查
if (blacklistedIps.contains(clientIp)) {
logSecurityEvent("IP黑名單攔截", clientIp, httpRequest);
sendErrorResponse(response, 403, "您的IP已被禁止訪問");
return;
}
// 2. 頻率限制檢查
if (isRateLimited(clientIp)) {
logSecurityEvent("頻率限制攔截", clientIp, httpRequest);
sendErrorResponse(response, 429, "訪問過于頻繁,請稍后再試");
return;
}
// 3. 可疑行為檢測
if (isSuspiciousRequest(clientIp, httpRequest)) {
logSecurityEvent("可疑請求攔截", clientIp, httpRequest);
blacklistedIps.add(clientIp); // 自動加入黑名單
sendErrorResponse(response, 403, "檢測到異常訪問行為");
return;
}
chain.doFilter(request, response);
}
private boolean isRateLimited(String ip) {
RateLimitInfo info = rateLimitMap.computeIfAbsent(ip, k -> new RateLimitInfo());
long currentTime = System.currentTimeMillis();
// 限制規(guī)則:每分鐘最多60次請求
if (currentTime - info.getWindowStart() > 60000) {
info.reset(60, currentTime);
}
return !info.tryAcquire();
}
private boolean isSuspiciousRequest(String ip, HttpServletRequest request) {
// 檢測異常User-Agent
String userAgent = request.getHeader("User-Agent");
if (userAgent == null || userAgent.trim().isEmpty()) {
return true;
}
// 檢測常見攻擊特征
String uri = request.getRequestURI().toLowerCase();
if (uri.contains("admin") || uri.contains("phpmyadmin") ||
uri.contains("wp-admin") || uri.contains("shell")) {
return true;
}
return false;
}
private void sendErrorResponse(ServletResponse response, int status, String message)
throws IOException {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(status);
httpResponse.setContentType("application/json;charset=utf-8");
httpResponse.getWriter().write("{\"code\": " + status + ", \"message\": \"" + message + "\"}");
}
private void logSecurityEvent(String event, String ip, HttpServletRequest request) {
logger.warn("安全事件: {} - IP: {}, URI: {}, User-Agent: {}",
event, ip, request.getRequestURI(), request.getHeader("User-Agent"));
}
// 頻率限制內(nèi)部類
private static class RateLimitInfo {
private int tokens;
private long windowStart;
private final int maxTokens = 60;
RateLimitInfo() {
reset(maxTokens, System.currentTimeMillis());
}
void reset(int tokens, long windowStart) {
this.tokens = tokens;
this.windowStart = windowStart;
}
boolean tryAcquire() {
if (tokens > 0) {
tokens--;
return true;
}
return false;
}
long getWindowStart() {
return windowStart;
}
}
}
五、實戰(zhàn)測試:驗證你的IP獲取是否正確
5.1 調(diào)試控制器
@RestController
@RequestMapping("/debug")
public class IpDebugController {
@GetMapping("/ip")
public Map<String, Object> debugIp(HttpServletRequest request) {
Map<String, Object> result = new LinkedHashMap<>();
// 真實IP
result.put("真實客戶端IP", IpUtils.getClientRealIp(request));
// 各種頭字段對比
result.put("RemoteAddr", request.getRemoteAddr());
result.put("X-Forwarded-For", request.getHeader("X-Forwarded-For"));
result.put("X-Real-IP", request.getHeader("X-Real-IP"));
result.put("Proxy-Client-IP", request.getHeader("Proxy-Client-IP"));
result.put("WL-Proxy-Client-IP", request.getHeader("WL-Proxy-Client-IP"));
// 請求詳細(xì)信息
result.put("請求方法", request.getMethod());
result.put("請求URI", request.getRequestURI());
result.put("User-Agent", request.getHeader("User-Agent"));
return result;
}
@GetMapping("/ip-headers")
public Map<String, String> getAllIpHeaders(HttpServletRequest request) {
Map<String, String> headers = new LinkedHashMap<>();
String[] ipHeaders = {
"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"
};
for (String header : ipHeaders) {
String value = request.getHeader(header);
if (value != null && !value.trim().isEmpty()) {
headers.put(header, value);
}
}
return headers;
}
}
5.2 測試用例
@SpringBootTest
class IpUtilsTest {
@Test
void testGetClientRealIp() {
// 模擬HttpServletRequest
MockHttpServletRequest request = new MockHttpServletRequest();
// 測試場景1:直接訪問
request.setRemoteAddr("123.45.67.89");
assertEquals("123.45.67.89", IpUtils.getClientRealIp(request));
// 測試場景2:單層代理
request.addHeader("X-Forwarded-For", "123.45.67.89");
request.setRemoteAddr("10.0.0.1");
assertEquals("123.45.67.89", IpUtils.getClientRealIp(request));
// 測試場景3:多層代理
request.addHeader("X-Forwarded-For", "123.45.67.89, 10.0.1.100, 10.0.1.101");
assertEquals("123.45.67.89", IpUtils.getClientRealIp(request));
// 測試場景4:IPv6
request.addHeader("X-Forwarded-For", "2001:db8::1");
assertEquals("2001:db8::1", IpUtils.getClientRealIp(request));
}
}
六、生產(chǎn)環(huán)境最佳實踐
6.1 配置管理
- 將信任的代理IP列表配置在配置中心,支持動態(tài)更新
- 為不同環(huán)境(開發(fā)、測試、生產(chǎn))設(shè)置不同的代理配置
6.2 監(jiān)控告警
@Component
public class IpMonitor {
@EventListener
public void handleBlacklistEvent(BlacklistEvent event) {
// 發(fā)送告警通知
alertService.sendAlert("IP黑名單新增: " + event.getIp());
}
@Scheduled(fixedRate = 300000) // 5分鐘執(zhí)行一次
public void cleanupRateLimit() {
// 定期清理過期的頻率限制記錄
}
}
6.3 性能優(yōu)化
- 對于高并發(fā)場景,使用Redis實現(xiàn)分布式頻率限制
- 對IP查詢結(jié)果進(jìn)行適當(dāng)緩存(注意緩存時間不宜過長)
七、常見問題排查
Q1: 為什么獲取到的還是127.0.0.1?
A: 檢查負(fù)載均衡器是否正確配置了X-Forwarded-For頭。
Q2: 多級代理下如何確定真實IP?
A: 使用本文提供的parseXForwardedFor方法,它會自動處理多級代理情況。
Q3: 如何防止IP偽造?
A: 配置internal-proxies只信任內(nèi)部代理服務(wù)器,不信任客戶端傳遞的頭部。
總結(jié)
獲取真實客戶端IP是Web開發(fā)中的基礎(chǔ)但重要的工作。通過本文的完整方案,你可以:
- 正確獲取多級代理后的真實客戶端IP
- 有效防止IP地址偽造攻擊
- 實現(xiàn)IP級別的安全防護(hù)
- 具備完整的監(jiān)控和調(diào)試能力
記?。?strong>不要相信客戶端傳遞的任何信息,始終通過可信的代理服務(wù)器頭字段來獲取真實IP。
以上就是SpringBoot中獲取真實客戶端IP的終極方案的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot獲取客戶端IP的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring Boot 2 Thymeleaf服務(wù)器端表單驗證實現(xiàn)詳解
這篇文章主要介紹了Spring Boot 2 Thymeleaf服務(wù)器端表單驗證實現(xiàn)詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-11-11
Spring MVC 擴(kuò)展和 SSM 框架整合步驟詳解
在前端頁面后后臺交互的過程中,需要一種格式清晰、高效且兩端都可以輕松使用的數(shù)據(jù)格式做交互的媒介,JSON正可以滿足這一需求,下面學(xué)習(xí)使用Spring MVC 框架處理JSON數(shù)據(jù),感興趣的朋友一起看看吧2024-08-08
java調(diào)用webservice接口,并解析返回參數(shù)問題
這篇文章主要介紹了java調(diào)用webservice接口,并解析返回參數(shù)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07
Java并發(fā)編程加鎖導(dǎo)致的活躍性問題詳解方案
所謂并發(fā)編程是指在一臺處理器上"同時"處理多個任務(wù)。并發(fā)是在同一實體上的多個事件。多個事件在同一時間間隔發(fā)生,所以編寫正確的程序很難,而編寫正確的并發(fā)程序則難上加難2021-10-10

