springboot gateway + nginx + k8s部署如何获取客户端真实ip

springboot gateway + nginx + k8s部署如何获取客户端真实ip


新的需求需要对某个接口进行访问ip的限制,但是由于部署环境问题,常用的获取ip方式拿到的都是k8s服务的地址,记录一下在k8s+nginx+gateway的情况下获取真实ip的过程

k8s获取真实ip

通常,当 Kubernetes 集群内的客户端连接到服务的时候,是支持服务的 Pod 可以获取到客户端的 IP 地址的,但是,当通过节点端口接收到连接时,由于对数据包执行了源网络地址转换(SNAT),因此数据包的源 IP 地址会发生变化,后端的 Pod 无法看到实际的客户端 IP,对于某些应用来说是个问题,比如,nginx 的请求日志就无法获取准确的客户端访问 IP 了

通过查看日志就能看到nginx日志中获取到的浏览器访问ip是master节点的内网地址,并不是我们期望的真实的浏览器访问ip

我这里是通过nginx的服务对外开放的访问端口,所以要改这个服务的service

spec:
  externalTrafficPolicy: Local

如果 Service 中配置了 externalTrafficPolicy=Local,并且通过服务的节点端口来打开外部连接,则 Service 会代理到本地运行的 Pod,如果本地没有本地 Pod 存在,则连接将挂起,比如我们这里设置上该字段更新,这个时候我们去通过 master 节点的 NodePort 访问应用是访问不到的,因为 master 节点上并没有对应的 Pod 运行,所以需要确保负载均衡器将连接转发给至少具有一个 Pod 的节点。

但是需要注意的是使用这个参数有一个缺点,通常情况下,请求都是均匀分布在所有 Pod 上的,但是使用了这个配置的话,情况就有可能不一样了。比如我们有两个节点上运行了 3 个 Pod,假如节点 A 运行一个 Pod,节点 B 运行两个 Pod,如果负载均衡器在两个节点间均衡分布连接,则节点 A 上的 Pod 将接收到所有请求的 50%,但节点 B 上的两个 Pod 每个就只接收 25% 。

由于增加了externalTrafficPolicy: Local这个配置后,接收请求的节点和目标 Pod 都在一个节点上,所以没有额外的网络跳转(不执行 SNAT),所以就可以拿到正确的客户端 IP

更新服务后,然后再通过 NodePort 访问服务可以看到nginx日志拿到的就是正确的客户端 IP 地址了

ingress的方式没有实践过,查到是把ingress controller的externalTrafficPolicy设为Local,有需要的可以尝试一下

nginx配置

其实这一步不配也是可以的,做完k8s的配置就已经能拿到真实ip了,但有的时候会遇到某个模块的接口里也需要拿真实ip,但是从gateway到某个模块的过程中,在k8s里请求是从gateway的服务转发到相关模块的服务的,这也就导致相关模块获取到的真实ip是gateway服务的ip(或者是nginx服务的ip来着,反正不是上一步拿到的ip)
需要增加如图所示配置,后端获取ip时记得把X-Forwarded-For提到最前面呀,要不就白配置了
我这里的nginx只有一层,至于多层的需要自己测一下是不是需要每层都配还是只配第一层
在这里插入图片描述

gateway获取ip

这个就直接上代码吧

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;

@Slf4j
public class WebFluxUtil {

	private static final String IP_UNKNOWN = "unknown";
	private static final String IP_LOCAL = "127.0.0.1";
	private static final String IPV6_LOCAL = "0:0:0:0:0:0:0:1";
 
    public static String getIpAddress(ServerHttpRequest request) {
        HttpHeaders headers = request.getHeaders();
        String ipAddress = headers.getFirst("x-forwarded-for");
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = headers.getFirst("X-Real-Ip");
        }
        if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
            ipAddress = Optional.ofNullable(request.getRemoteAddress())
            		.map(address -> address.getAddress().getHostAddress())
            		.orElse("");
            if (IP_LOCAL.equals(ipAddress) || IPV6_LOCAL.equals(ipAddress)) {
                // 根据网卡取本机配置的IP
                try {
                    InetAddress inet = InetAddress.getLocalHost();
                    ipAddress = inet.getHostAddress();
                } catch (UnknownHostException e) {
                    log.error(e.getMessage());
                }
            }
        }
 
        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.indexOf(",") > 0) {
            ipAddress = ipAddress.split(",")[0];
        }
 
        return ipAddress;
    }
}

gateway限制访问ip

import com.plat.gateway.config.properties.IgnoreWhiteProperties;
import com.plat.gateway.enums.ExceptionCodeEnum;
import com.plat.gateway.util.WebFluxUtil;
import com.ruoyi.common.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Component
@Slf4j
public class CheckIPFilter implements GlobalFilter, Ordered {

	@Autowired
	private IgnoreWhiteProperties ignoreWhiteProperties;
	
	@Override
	public int getOrder() { 
		return 0; 
	}
	@Override
	public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
		String ip = WebFluxUtil.getIpAddress(exchange.getRequest());
		if (StringUtils.isEmpty(ip)){
			ExceptionCodeEnum.GET_IP_ERROR.newException();
		}
		//对 IP 的访问限制,即不在 IP 白名单中就不能调用受ip访问限制的接口
		String path = exchange.getRequest().getURI().getPath();
		if (ignoreWhiteProperties.getBlockchain().contains(path)) {
			if (!ignoreWhiteProperties.getBlockchainIp().contains(ip)){
				log.info("不被允许的ip:{},path:{}",ip,path);
				ExceptionCodeEnum.IP_ERROR.newException();
			}else
				log.info("访问ip:{},path:{}",ip,path);
		}
		return chain.filter(exchange);
	}
}

这里是把nacos里配置的白名单单独放了一个类,这里也写一下吧

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.List;

/**
 * 放行白名单配置
 */
@Configuration
@RefreshScope
@ConfigurationProperties(prefix = "security.ignore")
public class IgnoreWhiteProperties
{
	/**
	 * 放行白名单配置,网关不校验此处的白名单
	 */
	private List<String> whites = new ArrayList<>();
	/**
	 * 限制ip访问接口
	 */
	private List<String> blockchain = new ArrayList<>();
	
	private List<String> blockchainIp = new ArrayList<>();
	
	public List<String> getWhites() 
	{ 
		return whites; 
	}
	
	public void setWhites(List<String> whites) 
	{ 
		this.whites = whites; 
	}
	
	public List<String> getBlockchain()
	{
		return blockchain;
	}
	
	public void setBlockchain(List<String> blockchain) 
	{ 
		this.blockchain = blockchain; 
	}
	
	public List<String> getBlockchainIp() 
	{ 
		return blockchainIp; 
	}
	
	public void setBlockchainIp(List<String> blockchainIp) 
	{ 
		this.blockchainIp = blockchainIp;
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值