在后端 & 架构开发领域,获取客户端真实 IP 是许多功能(如访问日志、风控、地域统计)的基础。但这事儿的坑实在太多了。很多开发者写完 request.getRemoteAddr() 一测试,线上拿到的却全是负载均衡器或网关的内网 IP,根本抓不到用户来源。
更严重的是,如果解析逻辑不严谨,攻击者可以轻松通过伪造请求头来绕过风控和频率限制。今天我们就从底层原理到生产级代码,一次性把这件事儿搞透。
一、先搞懂:IP 传递的底层逻辑
现代 Web 系统的请求链路非常长,典型路径如下:
用户 → CDN → 负载均衡器 → 网关 → 应用服务器
每一层中间件都会修改请求的相关信息,这就是 getRemoteAddr() 失效的核心原因。应用服务器与用户之间隔着“千山万水”,getRemoteAddr() 只能拿到直连上游(即代理服务器)的 IP。
下面先搞清楚几个核心请求头的含义,可信度从高到低排列:
| 请求头字段 |
含义 |
可信度 |
X-Forwarded-For |
代理链 IP 序列 |
⭐⭐⭐⭐ |
X-Real-IP |
最后一个代理 IP |
⭐⭐⭐ |
Proxy-Client-IP |
Apache 代理 IP |
⭐⭐ |
WL-Proxy-Client-IP |
WebLogic 代理 IP |
⭐⭐ |
重中之重! X-Forwarded-For 是获取真实 IP 的核心字段,但 90% 的开发者都用错了!
该字段的标准格式为:
X-Forwarded-For: 客户端真实IP, 代理服务器1IP, 代理服务器2IP, ...
核心规则务必记牢:
- 最左侧的 IP 是原始客户端的真实 IP;
- 后续的 IP 是请求经过的各级代理服务器 IP;
- 多个 IP 之间用英文逗号分隔。
看几个实际业务场景的案例:
// 无代理场景
X-Forwarded-For: null
// 单代理场景
X-Forwarded-For: 123.45.67.89
// 两级代理场景
X-Forwarded-For: 123.45.67.89, 10.0.1.100
// 多级代理场景
X-Forwarded-For: 123.45.67.89, 203.0.113.195, 198.51.100.10
二、终极方案:生产级 IP 工具类(可直接复用)
下面这个工具类经过海量生产环境验证,解决了 IP 伪造、内网 IP 过滤、多级代理等常见问题,可以直接用在你的项目里。
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* IP工具类
* 功能:安全获取客户端真实IP,过滤内网IP、伪造IP,兼容多级代理场景
* 适用:Spring Boot/Spring MVC项目
*/
public class IpUtils {
// 未知IP标识
private static final String UNKNOWN = "unknown";
// 本地回环IP(IPv4)
private static final String LOCALHOST_IP = "127.0.0.1";
// 本地回环IP(IPv6)
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
// IP分隔符(X-Forwarded-For中多IP的分隔符)
private static final String SEPARATOR = ",";
// 内网IP段(需过滤的非公网IP)
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
* @param request HttpServletRequest请求对象
* @return 客户端真实IP(优先公网IP,无公网IP则返回内网/本地IP)
*/
public static String getClientRealIp(HttpServletRequest request) {
// 1. 优先解析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. 最后降级使用getRemoteAddr(大概率是代理IP)
ip = request.getRemoteAddr();
// 兼容IPv6本地回环地址转换
return LOCALHOST_IPV6.equals(ip) ? LOCALHOST_IP : ip;
}
/**
* 解析X-Forwarded-For头,提取有效IP
* 逻辑:从后往前过滤内网IP,优先返回第一个有效公网IP;无公网IP则返回第一个有效IP
* @param xffHeader X-Forwarded-For头值
* @return 解析后的IP(null表示无有效IP)
*/
private static String parseXForwardedFor(String xffHeader) {
// 空值直接返回null
if (xffHeader == null || xffHeader.trim().isEmpty()) {
return null;
}
// 按逗号分割多IP
String[] ips = xffHeader.split(SEPARATOR);
// 第一步:从后往前找第一个有效公网IP(过滤内网IP)
for (int i = ips.length - 1; i >= 0; i--) {
String ip = ips[i].trim();
// IP格式合法 + 非内网IP = 有效公网IP
if (isValidIp(ip) && !isInternalIp(ip)) {
return ip;
}
}
// 第二步:无公网IP时,返回第一个格式合法的IP(可能是内网IP)
for (String ip : ips) {
String trimmedIp = ip.trim();
if (isValidIp(trimmedIp)) {
return trimmedIp;
}
}
// 无任何有效IP
return null;
}
/**
* 从其他代理头中提取IP
* @param request HttpServletRequest请求对象
* @return 提取到的IP(null表示无有效IP)
*/
private static String getIpFromHeaders(HttpServletRequest request) {
// 常见的代理IP头字段列表
String[] headers = {
"X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP",
"HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"
};
// 遍历头字段,找到第一个有效IP
for (String header : headers) {
String ip = request.getHeader(header);
if (isValidIp(ip)) {
return ip;
}
}
return null;
}
/**
* 校验IP是否为有效格式(排除unknown、空值)
* @param ip 待校验IP
* @return true=有效,false=无效
*/
private static boolean isValidIp(String ip) {
return ip != null &&
!ip.isEmpty() &&
!UNKNOWN.equalsIgnoreCase(ip) &&
isValidIpAddress(ip);
}
/**
* 校验IP是否为有效公网IP
* @param ip 待校验IP
* @return true=有效公网IP,false=内网/本地/无效IP
*/
private static boolean isValidPublicIp(String ip) {
return isValidIp(ip) && !isInternalIp(ip) && !isLocalhost(ip);
}
/**
* 判断是否为内网IP
* @param ip 待判断IP
* @return true=内网IP,false=公网IP
*/
private static boolean isInternalIp(String ip) {
if (ip == null) return false;
// 匹配内网IP段前缀
return INTERNAL_IP_SEGMENTS.stream().anyMatch(ip::startsWith);
}
/**
* 判断是否为本地回环IP
* @param ip 待判断IP
* @return true=本地IP,false=非本地IP
*/
private static boolean isLocalhost(String ip) {
return LOCALHOST_IP.equals(ip) || LOCALHOST_IPV6.equals(ip);
}
/**
* 校验IP地址格式是否合法(支持IPv4/IPv6)
* @param ip 待校验IP
* @return true=格式合法,false=格式非法
*/
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 配置:让应用识别代理 IP
要让应用正确识别代理传递的真实 IP,必须配置 Tomcat 信任指定的内网代理,从而忽略客户端伪造的 X-Forwarded-For 头,只解析可信代理或负载均衡器设置的 X-Forwarded-For 头。
YAML 配置方式(推荐)
server:
tomcat:
# 配置Tomcat解析真实IP的核心参数
remoteip:
# 指定从X-Forwarded-For头获取真实IP
remote-ip-header: x-forwarded-for
# 指定从X-Forwarded-Proto头获取请求协议
protocol-header: x-forwarded-proto
# 信任的内网代理IP段(正则表达式)
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
如果想用 Java 代码配置,可以创建一个 WebServerFactoryCustomizer 的 Bean,配合 connector.setProperty 完成同样的设置,效果完全一致。

四、进阶功能:IP 拦截与安全防护
获取到真实 IP 只是第一步,下一步就是基于 IP 实现安全防护,比如黑名单、频率限制等。
1. IP 日志拦截器(记录访问日志)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* IP日志拦截器
* 功能:记录每个请求的真实IP、访问路径、客户端信息
*/
public class IpLoggingInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(IpLoggingInterceptor.class);
/**
* 请求处理前执行(记录IP日志)
*/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
// 获取真实IP并存入请求属性
String clientIp = IpUtils.getClientRealIp(request);
request.setAttribute("clientRealIp", clientIp);
// 记录访问日志(包含核心信息)
logger.info("客户端访问日志 - IP: {}, URI: {}, User-Agent: {}",
clientIp,
request.getRequestURI(),
request.getHeader("User-Agent"));
// 放行请求
return true;
}
}
然后在 WebMvcConfigurer 中注册该拦截器:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public IpLoggingInterceptor ipLoggingInterceptor() {
return new IpLoggingInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipLoggingInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/health", "/metrics");
}
}
2. IP 安全过滤器(黑名单 + 频率限制)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* IP安全过滤器
* 功能:IP黑名单拦截、访问频率限制、可疑请求检测
*/
public class IpSecurityFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(IpSecurityFilter.class);
// IP黑名单(线程安全)
private final Set<String> blacklistedIps = ConcurrentHashMap.newKeySet();
// 访问频率限制缓存(key=IP,value=频率限制信息)
private final ConcurrentMap<String, RateLimitInfo> rateLimitMap = new ConcurrentHashMap<>();
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 获取客户端真实IP
String clientIp = IpUtils.getClientRealIp(httpRequest);
// 1. 黑名单校验:命中则拦截
if (blacklistedIps.contains(clientIp)) {
logSecurityEvent("IP黑名单拦截", clientIp, httpRequest);
sendErrorResponse(httpResponse, 403, "您的IP已被禁止访问");
return;
}
// 2. 频率限制校验:访问过频则拦截
if (isRateLimited(clientIp)) {
logSecurityEvent("频率限制拦截", clientIp, httpRequest);
sendErrorResponse(httpResponse, 429, "访问过于频繁,请稍后再试");
return;
}
// 3. 可疑请求校验:检测异常行为
if (isSuspiciousRequest(clientIp, httpRequest)) {
logSecurityEvent("可疑请求拦截", clientIp, httpRequest);
blacklistedIps.add(clientIp); // 加入黑名单
sendErrorResponse(httpResponse, 403, "检测到异常访问行为,IP已被限制");
return;
}
// 所有校验通过,放行请求
chain.doFilter(request, response);
}
/**
* 频率限制校验
* 规则:1分钟内最多60次请求
*/
private boolean isRateLimited(String ip) {
RateLimitInfo info = rateLimitMap.computeIfAbsent(ip, k -> new RateLimitInfo());
long currentTime = System.currentTimeMillis();
// 时间窗口过期(超过1分钟),重置计数
if (currentTime - info.getWindowStart() > 60000) {
info.reset(60, currentTime);
}
// 尝试获取令牌:无令牌则触发限制
return !info.tryAcquire();
}
/**
* 可疑请求检测
* 规则:无User-Agent、访问敏感路径视为可疑
*/
private boolean isSuspiciousRequest(String ip, HttpServletRequest request) {
// 1. 无User-Agent视为可疑
String userAgent = request.getHeader("User-Agent");
if (userAgent == null || userAgent.trim().isEmpty()) {
return true;
}
// 2. 访问敏感路径视为可疑(后台管理、数据库管理等)
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(HttpServletResponse response, int status, String message)
throws IOException {
response.setStatus(status);
response.setContentType("application/json;charset=utf-8");
response.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"));
}
/**
* 频率限制信息封装
*/
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;
}
}
}
五、测试验证:确保 IP 获取准确
为了验证 IP 获取逻辑是否正确,我们可以编写一个调试接口和单元测试,覆盖不同场景。
1. IP 调试接口
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
public class IpDebugController {
@GetMapping("/debug/ip")
public Map<String, Object> debugIp(HttpServletRequest request) {
Map<String, Object> result = new LinkedHashMap<>();
// 核心:真实客户端IP
result.put("真实客户端IP", IpUtils.getClientRealIp(request));
// 对比:原生RemoteAddr
result.put("RemoteAddr", request.getRemoteAddr());
// 各IP头字段原始值
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"));
// 其他请求信息
result.put("请求方法", request.getMethod());
result.put("请求URI", request.getRequestURI());
result.put("User-Agent", request.getHeader("User-Agent"));
return result;
}
@GetMapping("/debug/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;
}
}
2. 单元测试
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.junit.jupiter.api.Assertions.assertEquals;
class IpUtilsTest {
@Test
void testGetClientRealIp() {
MockHttpServletRequest request = new MockHttpServletRequest();
// 场景1:无代理,直接访问
request.setRemoteAddr("123.45.67.89");
assertEquals("123.45.67.89", IpUtils.getClientRealIp(request));
// 场景2:单代理,X-Forwarded-For包含真实IP
request.addHeader("X-Forwarded-For", "123.45.67.89");
request.setRemoteAddr("10.0.0.1"); // 代理IP
assertEquals("123.45.67.89", IpUtils.getClientRealIp(request));
// 场景3:多级代理,X-Forwarded-For包含多个IP
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));
}
}
六、生产环境最佳实践
1. 动态配置信任代理 IP:将信任的代理 IP 列表存放在 Nacos 或 Apollo 等配置中心,支持动态更新,无需重启应用。
2. 环境隔离配置:开发、测试、生产环境需配置不同的代理规则。比如开发环境信任本机所有 IP,而生产环境仅信任指定的内网代理。
3. 分布式频率限制:在高并发场景下,要将频率限制逻辑迁移到 Redis 上,实现分布式限流,避免单机缓存失效导致限制不准确。
4. 合理缓存 IP 结果:对 IP 获取结果做短期缓存(比如 10 秒),以减少请求头的重复解析开销。但缓存时间不宜过长,避免 IP 变更后未能及时感知。
七、常见问题排查
1. 获取的 IP 还是代理 IP,不是真实用户 IP?
检查你的负载均衡器(如 Nginx)或网关是否正确配置了 X-Forwarded-For 头。确保代理确实把客户端真实 IP 写入了该字段。
2. 多级代理场景下,解析出的 IP 还是内网 IP?
直接使用本文提供的 parseXForwardedFor 方法。它从后往前遍历 IP 链,能自动过滤掉链上的所有内网 IP,优先返回合法的公网 IP。
3. 担心客户端伪造 X-Forwarded-For 头?
这是个大问题,也很容易解决。通过 internal-proxies 配置,明确告知 Tomcat 只信任来自内网代理转发的头信息。这样,凡是客户端请求中自带的 X-Forwarded-For 头都会被忽略。
总结
获取真实客户端 IP 是 Web 开发的基础但关键能力。错误的实现不仅让业务数据不准,更可能带来严重的安全风险。
通过本文的方案,你可以:精准解析多级代理下的真实 IP;有效过滤内网 IP 和伪造 IP;基于真实 IP 实现日志审计、黑名单和频率限制等安全措施。
记住核心原则:永远不要信任客户端直接传递的任何信息。所有 IP 相关的头字段,都必须经过你信任的代理服务器转发后,才能去解析。