Spring 客戶端 IP 地址獲取及儲存細節

sdbybyd發表於2020-08-04

原文連結: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 檔案

```c

define INET_ADDRSTRLEN (16)

define INET6_ADDRSTRLEN (48)

``` 最後一個字元為終結符,不算在內,最長為47字元

最終使用效果(@ClientIp 註解獲取):

```java 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

註解

```java 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)程式碼:

```java 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配置

```java 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?

相關文章