别再只用getRemoteAddr()了Spring Boot项目中获取真实客户端IP的完整指南含Nginx/CDN场景在分布式架构盛行的今天一个HTTP请求从用户浏览器到应用服务器可能经过CDN、负载均衡、API网关等多层网络设备。某次线上事故排查中我们发现安全审计日志记录的IP全是阿里云内网地址——这正是过度依赖request.getRemoteAddr()的典型后果。本文将带您穿透网络迷雾构建可靠的客户端IP提取体系。1. 为什么getRemoteAddr()在现代架构中失效当Tomcat服务器看到192.168.1.100这个IP时它记录的只是Nginx反向代理的地址而非真实用户IP。这种现象源于HTTP协议的特性经典三层代理架构用户(114.220.10.25) → CDN(203.107.33.100) → Nginx(172.18.5.6) → Spring Boot(192.168.1.100)关键头部传递链X-Forwarded-For: 114.220.10.25, 203.107.33.100, 172.18.5.6 X-Real-IP: 172.18.5.6主流代理设备的默认行为设备类型默认添加的头部典型值示例CloudflareCF-Connecting-IP用户真实IPAWS ALBX-Forwarded-For追加最新IPNginx需显式配置proxy_set_header依赖上游传递提示测试环境可通过curl模拟多级代理curl -H X-Forwarded-For: 1.1.1.1,2.2.2.2 http://your-api/2. 工业级IP提取工具类实现以下是通过实战检验的IP提取方案兼容主流云服务商场景public class IpUtils { private static final ListString IP_HEADERS Arrays.asList( X-Forwarded-For, Proxy-Client-IP, WL-Proxy-Client-IP, HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR, CF-Connecting-IP // Cloudflare专用 ); public static String extractClientIp(HttpServletRequest request) { for (String header : IP_HEADERS) { String ipList request.getHeader(header); if (StringUtils.isNotEmpty(ipList) !unknown.equalsIgnoreCase(ipList)) { return parseFirstValidIp(ipList); } } return request.getRemoteAddr(); } private static String parseFirstValidIp(String ipStr) { String[] ips ipStr.split(\\s*,\\s*); for (String ip : ips) { if (isValidIp(ip)) { return ip; } } return 127.0.0.1; } private static boolean isValidIp(String ip) { return !unknown.equalsIgnoreCase(ip) ip ! null ip.length() ! 0 !0:0:0:0:0:0:0:1.equals(ip); } }关键改进点多级代理处理正确解析X-Forwarded-For中的逗号分隔列表IPv6兼容保留原始格式而非强制转换云厂商适配支持阿里云、AWS、Cloudflare等特殊头部3. Nginx与Spring Boot的协同配置3.1 Nginx层关键配置location / { proxy_pass http://backend; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }$proxy_add_x_forwarded_for会自动追加而非覆盖现有值确保没有proxy_set_header X-Forwarded-For ;这样的重置操作3.2 Spring Boot应用配置在application.yml中启用ForwardedHeader处理server: forward-headers-strategy: framework或通过JavaConfigBean public ForwardedHeaderFilter forwardedHeaderFilter() { return new ForwardedHeaderFilter(); }4. 生产环境验证方案4.1 测试用例设计Test void shouldExtractCorrectIpFromMultiProxy() { MockHttpServletRequest request new MockHttpServletRequest(); request.setRemoteAddr(10.0.0.1); request.addHeader(X-Forwarded-For, 203.0.113.45, 198.51.100.22); String ip IpUtils.extractClientIp(request); assertEquals(203.0.113.45, ip); }4.2 日志审计验证建议在Filter中记录原始信息和提取结果Slf4j Component public class IpLogFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String clientIp IpUtils.extractClientIp(request); log.info(Raw IP: {} | Extracted IP: {} | Headers: {}, request.getRemoteAddr(), clientIp, Collections.list(request.getHeaderNames()) .stream() .collect(Collectors.toMap( name - name, name - request.getHeader(name) ))); chain.doFilter(request, response); } }5. 高级场景与安全考量5.1 防止IP伪造的防御策略public class IpWhitelistFilter extends OncePerRequestFilter { private final SetString allowedProxies Set.of(10.0.0.0/8, 172.16.0.0/12); Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String lastProxy getLastProxyIp(request); if (!isTrustedProxy(lastProxy)) { throw new SecurityException(Untrusted proxy detected); } chain.doFilter(request, response); } private boolean isTrustedProxy(String ip) { return allowedProxies.stream() .anyMatch(cidr - IpRange.fromCidr(cidr).contains(ip)); } }5.2 动态IP处理策略对于频繁变化的代理IP如移动运营商网络建议结合User-Agent和设备指纹进行综合判断使用IP地理位置库辅助验证对敏感操作启用二次认证public String getClientIdentifier(HttpServletRequest request) { String ip IpUtils.extractClientIp(request); String deviceId request.getHeader(X-Device-Fingerprint); return DigestUtils.md5Hex(ip | deviceId); }在Kubernetes环境中还需要特别注意Service Mesh的Sidecar代理可能引入的额外跳数。Istio等Service Mesh组件通常会在X-Forwarded-For头部追加自己的IP这要求我们的提取逻辑要兼容更多层的代理情况。
别再只用getRemoteAddr()了!Spring Boot项目中获取真实客户端IP的完整指南(含Nginx/CDN场景)
发布时间:2026/6/15 10:47:26
别再只用getRemoteAddr()了Spring Boot项目中获取真实客户端IP的完整指南含Nginx/CDN场景在分布式架构盛行的今天一个HTTP请求从用户浏览器到应用服务器可能经过CDN、负载均衡、API网关等多层网络设备。某次线上事故排查中我们发现安全审计日志记录的IP全是阿里云内网地址——这正是过度依赖request.getRemoteAddr()的典型后果。本文将带您穿透网络迷雾构建可靠的客户端IP提取体系。1. 为什么getRemoteAddr()在现代架构中失效当Tomcat服务器看到192.168.1.100这个IP时它记录的只是Nginx反向代理的地址而非真实用户IP。这种现象源于HTTP协议的特性经典三层代理架构用户(114.220.10.25) → CDN(203.107.33.100) → Nginx(172.18.5.6) → Spring Boot(192.168.1.100)关键头部传递链X-Forwarded-For: 114.220.10.25, 203.107.33.100, 172.18.5.6 X-Real-IP: 172.18.5.6主流代理设备的默认行为设备类型默认添加的头部典型值示例CloudflareCF-Connecting-IP用户真实IPAWS ALBX-Forwarded-For追加最新IPNginx需显式配置proxy_set_header依赖上游传递提示测试环境可通过curl模拟多级代理curl -H X-Forwarded-For: 1.1.1.1,2.2.2.2 http://your-api/2. 工业级IP提取工具类实现以下是通过实战检验的IP提取方案兼容主流云服务商场景public class IpUtils { private static final ListString IP_HEADERS Arrays.asList( X-Forwarded-For, Proxy-Client-IP, WL-Proxy-Client-IP, HTTP_CLIENT_IP, HTTP_X_FORWARDED_FOR, CF-Connecting-IP // Cloudflare专用 ); public static String extractClientIp(HttpServletRequest request) { for (String header : IP_HEADERS) { String ipList request.getHeader(header); if (StringUtils.isNotEmpty(ipList) !unknown.equalsIgnoreCase(ipList)) { return parseFirstValidIp(ipList); } } return request.getRemoteAddr(); } private static String parseFirstValidIp(String ipStr) { String[] ips ipStr.split(\\s*,\\s*); for (String ip : ips) { if (isValidIp(ip)) { return ip; } } return 127.0.0.1; } private static boolean isValidIp(String ip) { return !unknown.equalsIgnoreCase(ip) ip ! null ip.length() ! 0 !0:0:0:0:0:0:0:1.equals(ip); } }关键改进点多级代理处理正确解析X-Forwarded-For中的逗号分隔列表IPv6兼容保留原始格式而非强制转换云厂商适配支持阿里云、AWS、Cloudflare等特殊头部3. Nginx与Spring Boot的协同配置3.1 Nginx层关键配置location / { proxy_pass http://backend; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }$proxy_add_x_forwarded_for会自动追加而非覆盖现有值确保没有proxy_set_header X-Forwarded-For ;这样的重置操作3.2 Spring Boot应用配置在application.yml中启用ForwardedHeader处理server: forward-headers-strategy: framework或通过JavaConfigBean public ForwardedHeaderFilter forwardedHeaderFilter() { return new ForwardedHeaderFilter(); }4. 生产环境验证方案4.1 测试用例设计Test void shouldExtractCorrectIpFromMultiProxy() { MockHttpServletRequest request new MockHttpServletRequest(); request.setRemoteAddr(10.0.0.1); request.addHeader(X-Forwarded-For, 203.0.113.45, 198.51.100.22); String ip IpUtils.extractClientIp(request); assertEquals(203.0.113.45, ip); }4.2 日志审计验证建议在Filter中记录原始信息和提取结果Slf4j Component public class IpLogFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String clientIp IpUtils.extractClientIp(request); log.info(Raw IP: {} | Extracted IP: {} | Headers: {}, request.getRemoteAddr(), clientIp, Collections.list(request.getHeaderNames()) .stream() .collect(Collectors.toMap( name - name, name - request.getHeader(name) ))); chain.doFilter(request, response); } }5. 高级场景与安全考量5.1 防止IP伪造的防御策略public class IpWhitelistFilter extends OncePerRequestFilter { private final SetString allowedProxies Set.of(10.0.0.0/8, 172.16.0.0/12); Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String lastProxy getLastProxyIp(request); if (!isTrustedProxy(lastProxy)) { throw new SecurityException(Untrusted proxy detected); } chain.doFilter(request, response); } private boolean isTrustedProxy(String ip) { return allowedProxies.stream() .anyMatch(cidr - IpRange.fromCidr(cidr).contains(ip)); } }5.2 动态IP处理策略对于频繁变化的代理IP如移动运营商网络建议结合User-Agent和设备指纹进行综合判断使用IP地理位置库辅助验证对敏感操作启用二次认证public String getClientIdentifier(HttpServletRequest request) { String ip IpUtils.extractClientIp(request); String deviceId request.getHeader(X-Device-Fingerprint); return DigestUtils.md5Hex(ip | deviceId); }在Kubernetes环境中还需要特别注意Service Mesh的Sidecar代理可能引入的额外跳数。Istio等Service Mesh组件通常会在X-Forwarded-For头部追加自己的IP这要求我们的提取逻辑要兼容更多层的代理情况。