feat: 实现IP白名单严格拦截、三方Webhook多租户事件订阅与MCP专线业务调用
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
package com.iot.gateway.common;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
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";
|
||||
|
||||
/**
|
||||
* 获取客户端真实 IP(兼容多级 Nginx / 反向代理 / 负载均衡)
|
||||
*/
|
||||
public static String getClientIp(HttpServletRequest request) {
|
||||
if (request == null) {
|
||||
return UNKNOWN;
|
||||
}
|
||||
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (isInvalidIp(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (isInvalidIp(ip)) {
|
||||
ip = request.getHeader("Proxy-Client-IP");
|
||||
}
|
||||
if (isInvalidIp(ip)) {
|
||||
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,首个为客户端真实源 IP
|
||||
if (StringUtils.hasText(ip) && ip.contains(",")) {
|
||||
for (String subIp : ip.split(",")) {
|
||||
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 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;
|
||||
}
|
||||
if (!StringUtils.hasText(clientIp) || UNKNOWN.equalsIgnoreCase(clientIp)) {
|
||||
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();
|
||||
if (!pattern.isEmpty() && matchIp(normalizedClientIp, pattern)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单条规则匹配
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 IPv4 是否在 CIDR 网段内
|
||||
*/
|
||||
private static boolean isInCidrRange(String ipStr, 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (UnknownHostException | NumberFormatException e) {
|
||||
log.warn("CIDR 网段解析校验异常: ip={}, cidr={}, error={}", ipStr, cidr, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user