在Web开发中,获取客户端真实IP地址看似是一个简单的基础功能,但实际上却是一个隐藏着诸多陷阱的技术点。很多开发者在使用Spring Boot开发项目时,往往会直接调用request.getRemoteAddr()方法来获取客户端IP,直到项目上线后才发现获取到的都是负载均衡器或代理服务器的IP地址,而非真实用户IP。
更糟糕的是,有些开发者虽然知道要通过HTTP头信息来获取真实IP,但由于对代理链的工作原理理解不够深入,实现的方案存在严重的安全漏洞,可能被恶意用户通过伪造HTTP头信息来欺骗系统。
在现代分布式系统架构中,一个HTTP请求从客户端发出到最终到达应用服务器,通常要经过多个中间环节:
code复制用户浏览器 → CDN节点 → 负载均衡器(Nginx/HAProxy) → API网关 → 应用服务器
每个中间环节都会修改请求的TCP/IP连接信息,导致传统的getRemoteAddr()方法失效。具体来说:
getRemoteAddr()获取到的是CDN节点的IPgetRemoteAddr()获取到的是负载均衡器的IP如果错误地获取了客户端IP,可能会导致以下问题:
在代理环境中,有几个关键的HTTP头字段用于传递客户端真实IP信息:
| 头字段名称 | 说明 | 示例值 | 可信度 |
|---|---|---|---|
| X-Forwarded-For | 记录整个代理链的IP序列,最左侧是原始客户端IP | 203.0.113.1, 10.0.0.1 | ⭐⭐⭐⭐ |
| X-Real-IP | 通常记录最后一个代理服务器认定的客户端IP | 203.0.113.1 | ⭐⭐⭐ |
| Forwarded | 标准化代理头(RFC 7239),包含更丰富的代理信息 | for=203.0.113.1;proto=https | ⭐⭐⭐⭐ |
| Proxy-Client-IP | 老式代理头,主要用于Apache代理 | 203.0.113.1 | ⭐⭐ |
| WL-Proxy-Client-IP | WebLogic特有的代理头 | 203.0.113.1 | ⭐⭐ |
X-Forwarded-For(XFF)是最常用也最重要的代理头字段,它的格式规范如下:
code复制X-Forwarded-For: client, proxy1, proxy2, ...
直接访问(无代理):
code复制X-Forwarded-For: null
此时应使用getRemoteAddr()
经过CDN:
code复制X-Forwarded-For: 203.0.113.1
203.0.113.1就是真实客户端IP
CDN + Nginx负载均衡:
code复制X-Forwarded-For: 203.0.113.1, 10.0.1.100
203.0.113.1是客户端IP,10.0.1.100是CDN节点IP
复杂代理链:
code复制X-Forwarded-For: 203.0.113.1, 198.51.100.10, 10.0.1.100
203.0.113.1是客户端IP,后面两个是代理服务器IP
在使用X-Forwarded-For时,必须注意以下安全问题:
因此,在实现IP获取逻辑时,必须:
以下是经过生产环境验证的IP工具类完整实现,包含了所有必要的安全检查和验证逻辑:
java复制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_IPV4 = "127.0.0.1";
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
private static final String SEPARATOR = ",";
// 内网IP段(用于识别和过滤代理服务器IP)
private static final Set<String> INTERNAL_IP_PREFIXES = 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地址
*/
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. 最后使用RemoteAddr
ip = request.getRemoteAddr();
return LOCALHOST_IPV6.equals(ip) ? LOCALHOST_IPV4 : ip;
}
/**
* 解析X-Forwarded-For头(核心逻辑)
* 采用从右向左查找第一个公网IP的策略,更安全可靠
*/
private static String parseXForwardedFor(String xffHeader) {
if (xffHeader == null || xffHeader.trim().isEmpty()) {
return null;
}
String[] ips = xffHeader.split(SEPARATOR);
// 从右向左查找第一个公网IP(避免客户端伪造)
for (int i = ips.length - 1; i >= 0; i--) {
String ip = ips[i].trim();
if (isValidIp(ip) && !isInternalIp(ip)) {
return ip;
}
}
// 如果没有公网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[] headersToCheck = {
"X-Real-IP",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_CLIENT_IP",
"HTTP_X_FORWARDED_FOR"
};
for (String header : headersToCheck) {
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);
}
/**
* 验证是否为公网IP
*/
private static boolean isValidPublicIp(String ip) {
return isValidIp(ip) && !isInternalIp(ip) && !isLocalhost(ip);
}
/**
* 检查是否为内网IP
*/
private static boolean isInternalIp(String ip) {
if (ip == null) return false;
return INTERNAL_IP_PREFIXES.stream().anyMatch(ip::startsWith);
}
/**
* 检查是否为本地回环地址
*/
private static boolean isLocalhost(String ip) {
return LOCALHOST_IPV4.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简化验证
String ipv6Pattern = "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$";
if (ip.matches(ipv6Pattern)) return true;
// 支持压缩形式的IPv6
String ipv6CompressedPattern = "^(([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4})*)?)::(([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4})*)?)$";
return ip.matches(ipv6CompressedPattern);
}
}
传统的X-Forwarded-For解析是从左向右取第一个IP作为客户端IP,但这种做法存在安全隐患:
更安全的做法是从右向左查找第一个非内网IP:
工具类中定义了常见的内网IP段(RFC 1918):
过滤这些IP段可以:
工具类采用三级检查策略:
这种策略确保了在各种环境下都能获取到最可靠的IP地址。
要让Tomcat正确识别代理头信息,需要进行以下配置:
java复制import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@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");
// 配置信任的内网代理(正则表达式)
connector.setProperty("internalProxies",
"192\\.168\\.\\d{1,3}\\.\\d{1,3}|" + // 192.168.0.0/16
"10\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}|" + // 10.0.0.0/8
"172\\.(1[6-9]|2[0-9]|3[0-1])\\.\\d{1,3}\\.\\d{1,3}"); // 172.16.0.0/12
// 代理主机头(可选)
connector.setProperty("hostHeader", "x-forwarded-host");
});
}
}
除了代码配置,也可以通过application.yml进行配置:
yaml复制server:
tomcat:
remoteip:
remote-ip-header: x-forwarded-for # 代理IP头
protocol-header: x-forwarded-proto # 协议头
internal-proxies: | # 信任的内网代理正则
192\.168\.\d{1,3}\.\d{1,3}| # 192.168.0.0/16
10\.\d{1,3}\.\d{1,3}\.\d{1,3}| # 10.0.0.0/8
172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3} # 172.16.0.0/12
protocol-header-https-value: https # 标识HTTPS请求的值
spring:
mvc:
log-request-details: true # 开启详细请求日志(调试用)
记录每个请求的客户端真实IP对于监控和审计非常重要:
java复制import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@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("Request from IP={}, URI={}, Method={}, User-Agent={}",
clientIp,
request.getRequestURI(),
request.getMethod(),
request.getHeader("User-Agent"));
return true;
}
}
import org.springframework.beans.factory.annotation.Autowired;
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 WebConfig implements WebMvcConfigurer {
@Autowired
private IpLoggingInterceptor ipLoggingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(ipLoggingInterceptor)
.addPathPatterns("/**") // 拦截所有路径
.excludePathPatterns("/health") // 排除健康检查
.excludePathPatterns("/favicon.ico"); // 排除favicon
}
}
实现基于IP的安全防护功能,包括黑名单和限流:
java复制import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
@Component
@Order(1) // 高优先级过滤器
public class IpSecurityFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(IpSecurityFilter.class);
// IP黑名单(生产环境应持久化到数据库)
private final Set<String> blacklistedIps = new ConcurrentSkipListSet<>();
// IP访问频率记录(生产环境建议使用Redis)
private final Map<String, RateLimitRecord> rateLimitRecords = new ConcurrentHashMap<>();
// 频率限制配置(每分钟最大请求数)
private static final int RATE_LIMIT = 100;
private static final long RATE_LIMIT_WINDOW_MS = 60_000; // 1分钟
@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("BLOCKED - Blacklisted IP", clientIp, httpRequest);
sendErrorResponse(response, HttpServletResponse.SC_FORBIDDEN,
"Your IP has been blocked");
return;
}
// 2. 频率限制检查
if (isOverRateLimit(clientIp)) {
logSecurityEvent("BLOCKED - Rate limit exceeded", clientIp, httpRequest);
sendErrorResponse(response, HttpServletResponse.SC_TOO_MANY_REQUESTS,
"Too many requests, please try again later");
return;
}
// 3. 可疑请求检测
if (isSuspiciousRequest(clientIp, httpRequest)) {
blacklistedIps.add(clientIp); // 自动加入黑名单
logSecurityEvent("BLOCKED - Suspicious request", clientIp, httpRequest);
sendErrorResponse(response, HttpServletResponse.SC_FORBIDDEN,
"Suspicious activity detected");
return;
}
chain.doFilter(request, response);
}
private boolean isOverRateLimit(String ip) {
RateLimitRecord record = rateLimitRecords.computeIfAbsent(
ip, k -> new RateLimitRecord());
long currentTime = System.currentTimeMillis();
// 如果当前时间超过时间窗口,重置计数器
if (currentTime - record.getWindowStart() > RATE_LIMIT_WINDOW_MS) {
record.reset(currentTime);
}
// 检查是否超过限制
if (record.getCount() >= RATE_LIMIT) {
return true;
}
// 增加计数
record.increment();
return false;
}
private boolean isSuspiciousRequest(String ip, HttpServletRequest request) {
// 检查User-Agent
String userAgent = request.getHeader("User-Agent");
if (userAgent == null || userAgent.isEmpty()) {
return true; // 没有User-Agent的请求可疑
}
// 检查常见攻击路径
String path = request.getRequestURI().toLowerCase();
if (path.contains("/admin") || path.contains("/phpmyadmin") ||
path.contains("/.env") || path.contains("/wp-login.php")) {
return true;
}
// 检查异常参数
Map<String, String[]> params = request.getParameterMap();
for (String[] values : params.values()) {
for (String value : values) {
if (value.contains("<script>") || value.contains("1=1")) {
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");
httpResponse.getWriter().write(
String.format("{\"status\":%d,\"message\":\"%s\"}", status, message));
}
private void logSecurityEvent(String event, String ip,
HttpServletRequest request) {
logger.warn("Security Event: {} - IP: {}, URI: {}, User-Agent: {}",
event, ip, request.getRequestURI(),
request.getHeader("User-Agent"));
}
/**
* 频率限制记录内部类
*/
private static class RateLimitRecord {
private int count;
private long windowStart;
RateLimitRecord() {
reset(System.currentTimeMillis());
}
void reset(long windowStart) {
this.count = 0;
this.windowStart = windowStart;
}
void increment() {
count++;
}
int getCount() {
return count;
}
long getWindowStart() {
return windowStart;
}
}
}
创建一个测试端点来验证IP获取功能:
java复制import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/api/ip")
public class IpTestController {
@GetMapping("/test")
public String testIp(HttpServletRequest request) {
String clientIp = IpUtils.getClientRealIp(request);
String method = request.getMethod();
String uri = request.getRequestURI();
return String.format("Client IP: %s, Method: %s, URI: %s",
clientIp, method, uri);
}
}
编写单元测试验证IP工具类的各种场景:
java复制import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.junit.jupiter.api.Assertions.*;
class IpUtilsTest {
@Test
void testDirectAccess() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("203.0.113.1");
assertEquals("203.0.113.1", IpUtils.getClientRealIp(request));
}
@Test
void testSingleProxy() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("10.0.0.1");
request.addHeader("X-Forwarded-For", "203.0.113.1");
assertEquals("203.0.113.1", IpUtils.getClientRealIp(request));
}
@Test
void testMultipleProxies() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("10.0.0.2");
request.addHeader("X-Forwarded-For", "203.0.113.1, 10.0.0.1");
assertEquals("203.0.113.1", IpUtils.getClientRealIp(request));
}
@Test
void testIpv6() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("2001:db8::1");
assertEquals("2001:db8::1", IpUtils.getClientRealIp(request));
}
@Test
void testFakeProxyHeader() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("10.0.0.1");
// 客户端伪造的XFF头
request.addHeader("X-Forwarded-For", "1.2.3.4, 203.0.113.1");
// 由于10.0.0.1是内网IP,我们的安全策略会忽略客户端提供的XFF头
assertEquals("10.0.0.1", IpUtils.getClientRealIp(request));
}
@Test
void testInvalidIp() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRemoteAddr("invalid.ip.address");
assertNull(IpUtils.getClientRealIp(request));
}
}
可以通过cURL命令模拟不同代理场景:
直接访问:
bash复制curl http://localhost:8080/api/ip/test
模拟代理头:
bash复制curl -H "X-Forwarded-For: 203.0.113.1" http://localhost:8080/api/ip/test
模拟多层代理:
bash复制curl -H "X-Forwarded-For: 203.0.113.1, 10.0.0.1, 192.168.1.1" http://localhost:8080/api/ip/test
模拟IPv6:
bash复制curl -H "X-Forwarded-For: 2001:db8::1" http://localhost:8080/api/ip/test
环境差异化配置:
动态配置:
配置验证:
关键指标监控:
告警规则:
java复制import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class IpMonitor {
private static final int BLACKLIST_ALERT_THRESHOLD = 10;
@Scheduled(fixedRate = 300000) // 每5分钟运行一次
public void checkBlacklist() {
int blacklistSize = ipSecurityFilter.getBlacklistedIps().size();
if (blacklistSize > BLACKLIST_ALERT_THRESHOLD) {
alertService.sendAlert(
String.format("黑名单IP数量异常: %d (阈值: %d)",
blacklistSize, BLACKLIST_ALERT_THRESHOLD));
}
}
}
Redis实现分布式限流:
IP信息缓存:
异步处理:
可能原因:
解决方案:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;option forwardfor可能原因:
解决方案:
可能原因:
解决方案:
可能原因:
解决方案:
最小信任原则:
深度防御:
定期审计:
Nginx安全配置:
nginx复制# 只允许受信任的代理设置X-Forwarded-For
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on; # 从右向左查找第一个非信任IP
Tomcat安全配置:
xml复制<!-- server.xml中的RemoteIpValve配置 -->
<Valve className="org.apache.catalina.valves.RemoteIpValve"
remoteIpHeader="x-forwarded-for"
internalProxies="10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}"
trustedProxies="203.0.113.1|198.51.100.1"
protocolHeader="x-forwarded-proto" />
应用层校验:
java复制// 在关键操作前进行额外IP验证
public void sensitiveOperation(HttpServletRequest request) {
String clientIp = IpUtils.getClientRealIp(request);
if (!isAllowedIp(clientIp)) {
throw new SecurityException("IP address not allowed: " + clientIp);
}
// 继续处理敏感操作
}
正则表达式开销:
内存使用:
同步阻塞:
预编译正则表达式:
java复制// 在工具类中预编译正则表达式
private static final Pattern IPV4_PATTERN = Pattern.compile(
"^((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]?)$");
private static final Pattern IPV6_PATTERN = Pattern.compile(
"^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");
// 使用时直接使用预编译的Pattern
boolean isIpv4 = IPV4_PATTERN.matcher(ip).matches();
使用高效的数据结构:
异步处理:
java复制@Component
public class AsyncIpLogger {
private final ExecutorService executor = Executors.newFixedThreadPool(2);
public void logRequestAsync(HttpServletRequest request) {
executor.submit(() -> {
String ip = IpUtils.getClientRealIp(request);
// 异步记录日志
logAccess(ip, request);
});
}
}
分布式限流:
java复制// 使用Redis实现分布式限流
public boolean tryAcquire(String ip) {
String key = "rate_limit:" + ip;
long current = System.currentTimeMillis();
// 使用Redis事务确保原子性
return redisTemplate.execute(new RedisCallback<Boolean>() {
@Override