Spring 客户端 IP 地址获取及存储细节

本文讲解如何在Spring框架内获取客户端 IP 地址以及存储的细节,常见使用场景如下:

  1. 网络安全,通常需要知道客户端请求的IP地址,以方便与已有的黑名单等进行对比,从而识别攻击
  2. 数据分析,记录用户登陆IP地址,识别用户地理位置,统计各省市用户数量等
  3. 请求限制,记录请求IP地址,限制请求频率

Spring 框架没有现成工具可以方便提取客户端的IP地址,普遍做法就是通过 HttpServletRequestgetRemoteAddr 方法获取IP地址。

存在以下问题:

  1. proxy:部分客户端使用代理后此方法返回的是代理网络的IP地址,非用户真实 IP
  2. SLB:后台经过负载均衡,如阿里云的SLB实例,方法返回地址是SLB实例 IP,并非用户真实 IP
  3. 环回地址:在本地测试时获取到的是ipv4:127.0.0.1 或者 ipv6:0:0:0:0:0:0:0:1,并非本机分配地址
  4. 代码简洁与耦合:每次获取地址都需要注入 HttpServletRequest 再提取,使用 Spring WebFlux 而不是Spring MVC,没有此对象可用
  5. 获取地址可能是IPv6 地址,长度不同,数据库需要兼容处理,适配以后 IPv6需求

问题解决:

  1. proxy :经过代理后通常可用通过 http header 的 Proxy-Client-IP 获取用户真实 IP地址
  2. SLB:经过SLB实例后可通过 http header 的 X-Forwarded-For 获取用户真实IP
  3. 环回地址:如果是环回地址,则根据网卡取本机配置的IP,如192.168.199.123 等
  4. 代码简洁与耦合:实现参数解析器,使用注解方式获取IP,如 @ClientIp
  5. 不同版本 IP 长度不同,取最长作为数据库存储长度(47最长)
版本 例子 字符长度
IPv4 192.168.199.111 15
IPv6 ABCD:ABCD:ABCD:ABCD:ABCD:ABCD:ABCD:ABCD 39
IPv4-mapped IPv6 ABCD:ABCD:ABCD:ABCD:ABCD:ABCD:192.168.158.190 45

注:IPv6 前后可能用:: 描述部分段,会增加2个字符,见 rfc6052
参考Linux系统下 inet.h 文件

#define INET_ADDRSTRLEN		(16)
#define INET6_ADDRSTRLEN	(48)

最后一个字符为终结符,不算在内,最长为47字符

最终使用效果(@ClientIp 注解获取):

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
@RequestMapping("/test")
@EnableAutoConfiguration
public class OrderController {

    @GetMapping("/hello")
    @ResponseBody
    @ResponseStatus(HttpStatus.OK)
    public String hello(@ClientIp String ip) {
        return "hello, ip = " + ip;
    }
}

实现代码

注:下面为 Spring MVC 下的实现代码,如需在Spring webFlux 下使用,同理实现下面方法、配置即可

org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver

org.springframework.web.reactive.config.WebFluxConfigurer

注解

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClientIp {

}

方法参数解析器(Resolver)代码:

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.ServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class ClientIpResolver implements HandlerMethodArgumentResolver {

    private static final String[] IP_HEADER_CANDIDATES = {
            "X-Forwarded-For",
            "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"
    };

    @Override
    public boolean supportsParameter(MethodParameter param) {
        return param.getParameterType().equals(String.class) &&
                param.hasParameterAnnotation(ClientIp.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) {
        // 提取header得到IP地址列表(多重代理场景),取第一个IP
        for (String header : IP_HEADER_CANDIDATES) {
            String ipList = webRequest.getHeader(header);
            if (ipList != null && ipList.length() != 0 &&
                    !"unknown".equalsIgnoreCase(ipList)) {
                return ipList.split(",")[0];
            }
        }

        // 没有经过代理或者SLB,直接 getRemoteAddr 方法获取IP
        String ip = ((ServletRequest) webRequest.getNativeRequest()).getRemoteAddr();

        // 如果是本地环回IP,则根据网卡取本机配置的IP
        if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
            try {
                InetAddress inetAddress = InetAddress.getLocalHost();
                return inetAddress.getHostAddress();
            } catch (UnknownHostException e) {
                e.printStackTrace();
                return ip;
            }
        }
        return ip;
    }
}

全局增加Resolver配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class NetWebMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(clientIpResolver());
    }

    @Bean
    public ClientIpResolver clientIpResolver() {
        return new ClientIpResolver();
    }
}

参考文档

  1. rfc6052 IPv4/IPv6 转换
  2. rfc1924 IPv6 地址格式
  3. Maximum length of the textual representation of an IPv6 address?

原文:taskhub.work