- 新增 ScopeMatcher、GatewayAuthorities、JwtClaims、GatewayHeaders、InternalPaths、McpProtocol/McpToolArgs 等常量与枚举 - Webhook 消息体改用 ObjectMapper 构建,修复原先仅转义引号的问题 - 管理接口与 OpenAPI 控制器的默认值、响应字段、演示数据集中到 AdminDefaults / OpenApiFields Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
214 lines
7.3 KiB
Java
214 lines
7.3 KiB
Java
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;
|
||
}
|
||
}
|
||
}
|