K8s Pod中应用如何获取客户端 IP

✨ 在 Kubernetes (K8s) 的 Pod 中运行的应用,想要获取客户端的真实 IP 地址信息,可以通过检查 "X-Original-Forwarded-For" 请求头来获取。在 K8s 集群中,请求经过 Ingress Controller 处理后,会将真实的客户端 IP 地址添加到该请求头中。因此,应用需要通过提取该请求头来获取客户端 IP 信息。需要注意的是,在 Ingress 资源中配置 "nginx.ingress.kubernetes.io/proxy-real-ip-cidr" 注解来启用对该请求头的支持。通过这种方式,应用可以准确获取到客户端的 IP 地址信息。

近期项目中需要记录下用户操作时IP信息的功能,在现有的项目架构中,客户端的IP信息会携带在HTTP请求头中。因该服务是Java实现的,即我们需要在 HttpServletRequest 中去提取 IP 信息。

IP工具类代码如下:

import javax.servlet.http.HttpServletRequest;

public class IPUtils {

    public static String getClientIP(HttpServletRequest request) {
        String ipAddress = request.getHeader("X-Forwarded-For");

        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
						ipAddress = request.getRemoteAddress().getAddress().getHostAddress();
            if (ipAddress.equals("127.0.0.1") || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                // 取本机IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    log.error("获取本机网卡IP异常", e);
                }
            }        
				}

        // 如果客户端 IP 是通过代理获取的,实际的客户端 IP 可能是以逗号分隔的,如果有多个代理。在这种情况下,第一个 IP 是实际的客户端 IP。
        int commaIndex = ipAddress.indexOf(",");
        if (commaIndex != -1) {
            ipAddress = ipAddress.substring(0, commaIndex);
        }

        return ipAddress;
    }
}

以上代码中有多个请求头名称,下面依次介绍:

  • X-Forwarded-For:是一个扩展头,用于识别通过HTTP代理或负载均衡器传输的客户端IP地址。当请求经过多个代理时,每个代理服务器都会将自己的IP地址添加到该头部的末尾。
  • Proxy-Client-IP:是一种特定于代理服务器的请求头,它用于指示连接到代理的客户端的IP地址。
  • WL-Proxy-Client-IP:是WebLogic服务器特定的请求头,用于获取连接到WebLogic服务器的客户端的IP地址。
  • HTTP_CLIENT_IP:是一种特定于代理服务器的请求头,用于指示连接到代理的客户端的IP地址。
  • HTTP_X_FORWARDED_FOR:是一种常见的请求头,用于指示连接到代理服务器的客户端的IP地址。

这些请求头中的信息多依赖于各类负载均衡器,代理服务器等的配置,因此想要获取到还需对访问链路中涉及到的相关服务进行配置。

此工具类在应用直接部署在Linux服务器上时,可以正常获取到,但应用生产环境是部署在K8s集群中的,此时的获取到IP为链路中Nginx服务所在主机的IP,并非是客户端IP,也就是代表上面几个HTTP请求头中未包含客户端IP信息。

查看应用在Pod中获取到的请求头信息,可通过代码日志打印实现,此处我借助如下开源项目来快速查看。

https://github.com/traefik/whoami

K8s 部署配置如下:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: whoami-deployment
spec:
  replicas: 1
  selector:
    matchLabels:
      app: whoami
  template:
    metadata:
      labels:
        app: whoami
    spec:
      containers:
      - name: whoami-container
        image: whoami:latest
---
apiVersion: v1
kind: Service
metadata:
  name: whoami-service
spec:
  ports:
  - name: http
    targetPort: 80
    port: 80
  selector:
    app: whoami
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: whoami-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
    - host: example.com
      http:
        paths:
        - path: /whoami(/|$)(.*)
          backend:
            serviceName: whoami-service
            servicePort: http

测试发现客户端 IP 信息只存在于 X-Original-Forwarded-For 中。故调整上面代码中增加一行对此请求头信息的提取即可。

if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
    ipAddress = request.getHeader("X-Original-Forwarded-For");
}

需求是解决了,那么为什么在 K8s 中时,客户端 IP 的信息就只在 X-Original-Forwarded-For 请求头中了呢?

下面是访问链路的简化示意图

                        +---------------------------+
                        |                           |
                        |        客户端              |
                        |                           |
                        +--------------+------------+
                                       |
                                       v
                     +----------------------------------+
                     |                                  |
                     |       ISP 路由                    |
                     |                                  |
                     +----------------------------------+
                                       |
                                       v
            +----------------------------------------------------+
            |                                                    |
            |                  域名解析IP所在机器(负载均衡)         |
            |                                                    |
            +-------------+---------------------+---------------+
                          |                     |
                          v                     v
            +-----------------------------------+----------------+
            |                                   |                |
            |               WAF                 |      SLB       |
            |                                   |                |
            +---------------+-------------------+----------------+
                            |                     |
                            v                     v
            +----------------------------------------------------+
            |                                                    |
            |              Kubernetes Ingress                    |
            |                                                    |
            +---------------+--------------------+---------------+
                            |                    |
                            v                    v
            +----------------------------------------------------+
            |                                                    |
            |                 Kubernetes Service                  |
            |                                                    |
            +---------------+--------------------+---------------+
                            |                    |
                            v                    v
            +-----------------------------------+----------------+
            |                                   |                |
            |               Kubernetes Pod      |    Container   |
            |                                   |                |
            +-----------------------------------+----------------+
                            |                    |
                            v                    v
            +----------------------------------------------------+
            |                                                    |
            |                      应用                           |
            |                                                    |
            +----------------------------------------------------+

K8s 部署下与直接部署的区别就是流量需要经过 K8s Ingress 的转发。Ingress Controller 是Kubernetes集群中用于管理入站网络流量的组件,它充当着入口点和流量路由器的角色。当请求到达Ingress控制器时,它会检查原始的请求头信息,并根据配置进行适当的处理和转发。通常情况下,X-Original-Forwarded-For 请求头是在 Ingress Controller 读取原有的请求头信息来设置的,因此请求最终到达应用时我们需要通过 X-Original-Forwarded-For 请求头来提取IP信息。

参考

更多相关信息

当使用Kubernetes集群和NGINX Ingress Controller 时,x-original-forwarded-for是一个HTTP请求标头,用于传递原始客户端IP地址。它被添加到传入请求的标头中,以便应用程序可以获取到真实的客户端IP地址,尤其在经过代理或负载均衡器之后。

使用NGINX Ingress控制器时,你可以通过在Ingress资源中配置nginx.ingress.kubernetes.io/proxy-real-ip-cidr注解来启用x-original-forwarded-for标头的支持。例如:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  annotations:
    nginx.ingress.kubernetes.io/proxy-real-ip-cidr: "0.0.0.0/0"
spec:
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-service
                port:
                  number: 80

在上述示例中,nginx.ingress.kubernetes.io/proxy-real-ip-cidr设置为0.0.0.0/0,这将允许任何IP地址发送x-original-forwarded-for标头。可以根据需要设置特定的CIDR。