package com.iot.gateway.common; import jakarta.servlet.http.HttpServletRequest; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Arrays; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import lombok.extern.slf4j.Slf4j; import org.springframework.util.StringUtils; /** * IP 获取与白名单校验工具类 (具备可信反向代理校验防御) */ @Slf4j public class IpUtils { private static final String UNKNOWN = "unknown"; private static final String LOCALHOST_IPV4 = "127.0.0.1"; private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1"; private static final String WILDCARD = "*"; private static final String ANY_CIDR = "0.0.0.0/0"; private static final String CIDR_SEPARATOR = "/"; private static final String IP_LIST_SEPARATOR = ","; /** 代理配置的分隔符:逗号、分号、空白 */ private static final String PROXY_CONFIG_SPLIT_REGEX = "[,;\\s]+"; /** 白名单配置的分隔符:逗号、分号、中文逗号、换行 */ private static final String WHITELIST_SPLIT_REGEX = "[,;,\\n\\r]+"; private static final int BITS_PER_BYTE = 8; private static final int BYTE_MASK = 0xFF; private static final int CIDR_PART_COUNT = 2; /** * 默认信任的内网/本地反向代理 IP 网段 (如本地 Nginx / 宿主机代理) */ private static final List TRUSTED_PROXIES = new CopyOnWriteArrayList<>(Arrays.asList( "127.0.0.1", "::1", "0:0:0:0:0:0:0:1", "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" )); public static void setTrustedProxies(String proxiesConfig) { if (StringUtils.hasText(proxiesConfig)) { TRUSTED_PROXIES.clear(); for (String p : proxiesConfig.split(PROXY_CONFIG_SPLIT_REGEX)) { if (StringUtils.hasText(p)) { TRUSTED_PROXIES.add(p.trim()); } } } } /** * 获取客户端真实 IP(仅在直接对端 RemoteAddr 属于可信反向代理时,才信任反代传递的 IP 头) */ public static String getClientIp(HttpServletRequest request) { if (request == null) { return UNKNOWN; } String remoteAddr = request.getRemoteAddr(); if (LOCALHOST_IPV6.equals(remoteAddr)) { remoteAddr = LOCALHOST_IPV4; } // 核心安全防伪造:如果直连来源不是受信任的反向代理,直接使用 remoteAddr,严禁采信伪造的 X-Forwarded-For 等头部 if (!isTrustedProxy(remoteAddr)) { return remoteAddr != null ? remoteAddr.trim() : UNKNOWN; } // 仅在确认对端为受信任反向代理的前提下,提取代理传递的源 IP String ip = request.getHeader(GatewayHeaders.FORWARDED_FOR); if (isInvalidIp(ip)) { ip = request.getHeader(GatewayHeaders.REAL_IP); } if (isInvalidIp(ip)) { ip = request.getHeader(GatewayHeaders.PROXY_CLIENT_IP); } if (isInvalidIp(ip)) { ip = request.getHeader(GatewayHeaders.WL_PROXY_CLIENT_IP); } if (isInvalidIp(ip)) { ip = remoteAddr; } // 多级反向代理会有多个逗号分隔的 IP,首个为客户端真实源 IP if (StringUtils.hasText(ip) && ip.contains(IP_LIST_SEPARATOR)) { for (String subIp : ip.split(IP_LIST_SEPARATOR)) { String trimmed = subIp.trim(); if (!isInvalidIp(trimmed)) { ip = trimmed; break; } } } if (LOCALHOST_IPV6.equals(ip)) { ip = LOCALHOST_IPV4; } return ip != null ? ip.trim() : UNKNOWN; } private static boolean isTrustedProxy(String ip) { if (!StringUtils.hasText(ip) || UNKNOWN.equalsIgnoreCase(ip)) { return false; } String normalized = LOCALHOST_IPV6.equals(ip.trim()) ? LOCALHOST_IPV4 : ip.trim(); for (String trustedPattern : TRUSTED_PROXIES) { if (matchIp(normalized, trustedPattern)) { return true; } } return false; } private static boolean isInvalidIp(String ip) { return !StringUtils.hasText(ip) || UNKNOWN.equalsIgnoreCase(ip.trim()); } /** * 校验客户端 IP 是否匹配白名单 */ public static boolean isAllowed(String clientIp, String whitelist) { if (!StringUtils.hasText(whitelist)) { return true; } if (!StringUtils.hasText(clientIp) || UNKNOWN.equalsIgnoreCase(clientIp)) { return false; } String normalizedClientIp = LOCALHOST_IPV6.equals(clientIp.trim()) ? LOCALHOST_IPV4 : clientIp.trim(); String[] rules = whitelist.split(WHITELIST_SPLIT_REGEX); for (String rule : rules) { String pattern = rule.trim(); if (!pattern.isEmpty() && matchIp(normalizedClientIp, pattern)) { return true; } } return false; } /** * IP 规则匹配:支持精确 IP、通配符(192.168.1.*)、CIDR 网段(192.168.1.0/24) */ public static boolean matchIp(String ip, String pattern) { if (ip == null || pattern == null) { return false; } pattern = pattern.trim(); ip = ip.trim(); if (LOCALHOST_IPV6.equals(ip)) ip = LOCALHOST_IPV4; if (LOCALHOST_IPV6.equals(pattern)) pattern = LOCALHOST_IPV4; if (WILDCARD.equals(pattern) || ANY_CIDR.equals(pattern)) { return true; } if (pattern.contains(CIDR_SEPARATOR)) { return matchCidr(ip, pattern); } if (pattern.contains(WILDCARD)) { return matchWildcard(ip, pattern); } return ip.equalsIgnoreCase(pattern); } private static boolean matchWildcard(String ip, String pattern) { String regex = pattern.replace(".", "\\.").replace(WILDCARD, ".*"); return ip.matches(regex); } private static boolean matchCidr(String ip, String cidr) { try { String[] parts = cidr.split(CIDR_SEPARATOR); if (parts.length != CIDR_PART_COUNT) return false; String network = parts[0]; int prefixLength = Integer.parseInt(parts[1]); byte[] ipBytes = InetAddress.getByName(ip).getAddress(); byte[] netBytes = InetAddress.getByName(network).getAddress(); if (ipBytes.length != netBytes.length) return false; int byteCount = prefixLength / BITS_PER_BYTE; int bitCount = prefixLength % BITS_PER_BYTE; for (int i = 0; i < byteCount; i++) { if (ipBytes[i] != netBytes[i]) return false; } if (bitCount > 0 && byteCount < ipBytes.length) { int mask = (BYTE_MASK << (BITS_PER_BYTE - bitCount)) & BYTE_MASK; return (ipBytes[byteCount] & mask) == (netBytes[byteCount] & mask); } return true; } catch (UnknownHostException | NumberFormatException e) { log.warn("CIDR 解析错误: ip={}, cidr={}, err={}", ip, cidr, e.getMessage()); return false; } } }