fix: harden gateway auth and credential handling
This commit is contained in:
@@ -4,11 +4,13 @@ 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 获取与白名单校验工具类
|
||||
* IP 获取与白名单校验工具类 (具备可信反向代理校验防御)
|
||||
*/
|
||||
@Slf4j
|
||||
public class IpUtils {
|
||||
@@ -18,13 +20,47 @@ public class IpUtils {
|
||||
private static final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
|
||||
|
||||
/**
|
||||
* 获取客户端真实 IP(兼容多级 Nginx / 反向代理 / 负载均衡)
|
||||
* 默认信任的内网/本地反向代理 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("[,;\\s]+")) {
|
||||
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("X-Forwarded-For");
|
||||
if (isInvalidIp(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
@@ -36,13 +72,7 @@ public class IpUtils {
|
||||
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (isInvalidIp(ip)) {
|
||||
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||
}
|
||||
if (isInvalidIp(ip)) {
|
||||
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||
}
|
||||
if (isInvalidIp(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
ip = remoteAddr;
|
||||
}
|
||||
|
||||
// 多级反向代理会有多个逗号分隔的 IP,首个为客户端真实源 IP
|
||||
@@ -63,19 +93,27 @@ public class IpUtils {
|
||||
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 是否匹配白名单
|
||||
*
|
||||
* @param clientIp 客户端请求真实 IP
|
||||
* @param whitelist 白名单配置(支持英文逗号、分号、中文逗号、换行分隔;留空表示不限制;支持精确 IP、CIDR 网段如 192.168.1.0/24、通配符如 192.168.1.*)
|
||||
* @return true 允许通过,false 拦截
|
||||
*/
|
||||
public static boolean isAllowed(String clientIp, String whitelist) {
|
||||
// 白名单为空则不限制
|
||||
if (!StringUtils.hasText(whitelist)) {
|
||||
return true;
|
||||
}
|
||||
@@ -83,10 +121,8 @@ public class IpUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 统一规范化 localhost
|
||||
String normalizedClientIp = LOCALHOST_IPV6.equals(clientIp.trim()) ? LOCALHOST_IPV4 : clientIp.trim();
|
||||
|
||||
// 支持多种常见分隔符
|
||||
String[] rules = whitelist.split("[,;,\\n\\r]+");
|
||||
for (String rule : rules) {
|
||||
String pattern = rule.trim();
|
||||
@@ -99,75 +135,67 @@ public class IpUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条规则匹配
|
||||
* IP 规则匹配:支持精确 IP、通配符(192.168.1.*)、CIDR 网段(192.168.1.0/24)
|
||||
*/
|
||||
private static boolean matchIp(String clientIp, String pattern) {
|
||||
if ("*".equals(pattern)) {
|
||||
return true;
|
||||
}
|
||||
// 规范化 localhost
|
||||
if (("localhost".equalsIgnoreCase(pattern) || LOCALHOST_IPV6.equals(pattern)) && LOCALHOST_IPV4.equals(clientIp)) {
|
||||
return true;
|
||||
}
|
||||
// 1. 精确匹配
|
||||
if (pattern.equalsIgnoreCase(clientIp)) {
|
||||
return true;
|
||||
}
|
||||
// 2. 通配符匹配 (如 192.168.1.* -> 转换为 CIDR 192.168.1.0/24 逻辑)
|
||||
if (pattern.contains("*")) {
|
||||
String regex = pattern.replace(".", "\\.").replace("*", ".*");
|
||||
return clientIp.matches(regex);
|
||||
}
|
||||
// 3. CIDR 网段匹配 (如 192.168.1.0/24)
|
||||
if (pattern.contains("/")) {
|
||||
return isInCidrRange(clientIp, pattern);
|
||||
public static boolean matchIp(String ip, String pattern) {
|
||||
if (ip == null || pattern == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 ("*".equals(pattern) || "0.0.0.0/0".equals(pattern)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (pattern.contains("/")) {
|
||||
return matchCidr(ip, pattern);
|
||||
}
|
||||
|
||||
if (pattern.contains("*")) {
|
||||
return matchWildcard(ip, pattern);
|
||||
}
|
||||
|
||||
return ip.equalsIgnoreCase(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 IPv4 是否在 CIDR 网段内
|
||||
*/
|
||||
private static boolean isInCidrRange(String ipStr, String cidr) {
|
||||
private static boolean matchWildcard(String ip, String pattern) {
|
||||
String regex = pattern.replace(".", "\\.").replace("*", ".*");
|
||||
return ip.matches(regex);
|
||||
}
|
||||
|
||||
private static boolean matchCidr(String ip, String cidr) {
|
||||
try {
|
||||
String[] parts = cidr.split("/");
|
||||
if (parts.length != 2) {
|
||||
return false;
|
||||
}
|
||||
int maskBits = Integer.parseInt(parts[1].trim());
|
||||
if (maskBits < 0 || maskBits > 32) {
|
||||
return false;
|
||||
if (parts.length != 2) 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 / 8;
|
||||
int bitCount = prefixLength % 8;
|
||||
|
||||
for (int i = 0; i < byteCount; i++) {
|
||||
if (ipBytes[i] != netBytes[i]) return false;
|
||||
}
|
||||
|
||||
InetAddress targetAddr = InetAddress.getByName(ipStr);
|
||||
InetAddress subnetAddr = InetAddress.getByName(parts[0].trim());
|
||||
|
||||
byte[] targetBytes = targetAddr.getAddress();
|
||||
byte[] subnetBytes = subnetAddr.getAddress();
|
||||
|
||||
if (targetBytes.length != subnetBytes.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int fullBytes = maskBits / 8;
|
||||
for (int i = 0; i < fullBytes; i++) {
|
||||
if (targetBytes[i] != subnetBytes[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int remainingBits = maskBits % 8;
|
||||
if (remainingBits > 0 && fullBytes < targetBytes.length) {
|
||||
int mask = (0xFF << (8 - remainingBits)) & 0xFF;
|
||||
if ((targetBytes[fullBytes] & mask) != (subnetBytes[fullBytes] & mask)) {
|
||||
return false;
|
||||
}
|
||||
if (bitCount > 0 && byteCount < ipBytes.length) {
|
||||
int mask = (0xFF << (8 - bitCount)) & 0xFF;
|
||||
return (ipBytes[byteCount] & mask) == (netBytes[byteCount] & mask);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (UnknownHostException | NumberFormatException e) {
|
||||
log.warn("CIDR 网段解析校验异常: ip={}, cidr={}, error={}", ipStr, cidr, e.getMessage());
|
||||
log.warn("CIDR 解析错误: ip={}, cidr={}, err={}", ip, cidr, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user