Files
IoT-OpenApi/src/main/java/com/iot/gateway/common/IpUtils.java
T
wjhandClaude Sonnet 5 761636c2c5 refactor: 网关消除魔法值,scope/事件/请求头/MCP 协议等改为枚举与常量
- 新增 ScopeMatcher、GatewayAuthorities、JwtClaims、GatewayHeaders、InternalPaths、McpProtocol/McpToolArgs 等常量与枚举
- Webhook 消息体改用 ObjectMapper 构建,修复原先仅转义引号的问题
- 管理接口与 OpenAPI 控制器的默认值、响应字段、演示数据集中到 AdminDefaults / OpenApiFields

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-10 16:27:53 +08:00

214 lines
7.3 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String> 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;
}
}
}