找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4607

积分

0

好友

600

主题
发表于 半小时前 | 查看: 2| 回复: 0

后端 & 架构开发领域,获取客户端真实 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 完成同样的设置,效果完全一致。

ruoyi-vue-pro与yudao-cloud技术架构图及企业级支付退款与数据大屏界面

四、进阶功能: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 相关的头字段,都必须经过你信任的代理服务器转发后,才能去解析。




上一篇:独立开发者自述:靠小众工具软件月入3万,为何多数程序员不敢做?
下一篇:Censys报告:DarkSword iOS漏洞利用工具包波及180个Web资产与27台主机
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-8-6 04:34 , Processed in 1.102760 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表