Java IP 工具类实战:安全高效 pan-common IpUtils 全解析
·
Java IP 工具类实战:IpUtils 从格式校验到 SSRF 防御的完整方案
选题说明:IP 地址处理是后端开发的高频需求——日志中提取客户端 IP、接口限流白名单、子网计算、端口探测、IP 脱敏展示。JDK 的
InetAddress功能有限,大多数项目会自己拼工具类,但往往缺少安全加固(SSRF、DNS 注入)。IpUtils内置 13 项安全特性,覆盖 IPv4/IPv6 全场景,零外部依赖,是生产级的 IP 处理方案。
一、为什么需要统一的 IP 工具
在实际项目中,IP 处理的需求远比想象中复杂:
- 校验不严格:用正则校验 IP,
"01.02.03.04"通过了(前导零,可能触发八进制解析) - IPv6 处理薄弱:压缩格式
::1、带 Zone ID 的fe80::1%eth0不知道怎么校验 - CIDR 计算手写:子网掩码和 CIDR 互转,网络地址/广播地址计算,每次都翻文档
- 白名单/黑名单效率低:遍历 List 逐个比对,1000 条规则就卡顿
- 安全漏洞:端口探测接受主机名,攻击者可以传入
169.254.169.254(云元数据)做 SSRF
IpUtils 围绕这些问题做了系统性封装,零外部依赖,仅使用 JDK 标准库。
环境准备:
<!-- Spring Boot 2.x (javax) -->
<dependency>
<groupId>com.gitee.apanlh</groupId>
<artifactId>pan-common</artifactId>
<version>2.0.6</version>
</dependency>
<!-- Spring Boot 3.x (jakarta) -->
<dependency>
<groupId>com.gitee.apanlh</groupId>
<artifactId>pan-common</artifactId>
<version>3.0.6</version>
</dependency>
二、功能全景
| 分类 | 核心方法 | 说明 |
|---|---|---|
| 格式校验 | isIpV4() / isIpV6() / isIp() |
严格格式校验,含前导零拒绝 |
| 地址分类 | isPrivateIp() / isPublicIp() / isLoopbackAddress() 等 |
RFC 标准分类 |
| 数值转换 | toLongByIp() / toBytes() / maskToCidr() |
IP ↔ long / byte[] / CIDR 互转 |
| CIDR 子网 | isInSubnet() / getNetworkAddress() / getAvailableHostCount() |
子网匹配与计算 |
| IP 段解析 | matchIpRange() / countIpRange() / listIpRange() |
支持 CIDR/范围/通配符/单 IP |
| 批量匹配 | matchAny() / matchNone() |
白名单/黑名单校验 |
| IDC 探测 | isPortOpen() / measureLatency() / getReverseDns() |
端口/延迟/PTR(SSRF 加固) |
| 脱敏 | hideIp() / hideLocalIp() |
IP 地址脱敏展示 |
| 本机信息 | getLocalAddress() / getEffectiveAddresses() / getMacAddress() |
本机 IP / MAC |
三、格式校验
3.1 isIpV4 — IPv4 严格校验
isIpV4 使用 parseSegments 作为统一校验内核,内置多项安全检查:
// ========== 合法 ==========
IpUtils.isIpV4("192.168.1.1"); // true
IpUtils.isIpV4("0.0.0.0"); // true
IpUtils.isIpV4("255.255.255.255"); // true
// ========== 非法 ==========
IpUtils.isIpV4("256.1.1.1"); // false — 段值超 255
IpUtils.isIpV4("01.1.1.1"); // false — 前导零(防八进制歧义)
IpUtils.isIpV4("192.168.1"); // false — 仅 3 段
IpUtils.isIpV4("192.168.1.1.1"); // false — 5 段
IpUtils.isIpV4("192.168.1.a"); // false — 非数字
IpUtils.isIpV4(null); // false
IpUtils.isIpV4(""); // false
安全特性:
- 禁止前导零:
"01"非法,防止与八进制混淆 - 段值严格 0-255
- 必须恰好 4 段,每段 1-3 位全数字
3.2 isIpV6 — IPv6 校验(含 DNS 注入防御)
// ========== 合法 ==========
IpUtils.isIpV6("::1"); // true — 回环
IpUtils.isIpV6("2001:db8::1"); // true — 压缩格式
IpUtils.isIpV6("fe80::1%eth0"); // true — 带 Zone ID
IpUtils.isIpV6("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); // true — 完整格式
// ========== 非法(DNS 注入防御) ==========
IpUtils.isIpV6("localhost"); // false — 不触发 DNS 解析
IpUtils.isIpV6("google.com"); // false — 不触发 DNS 解析
IpUtils.isIpV6("192.168.1.1"); // false — 这是 IPv4
安全设计:
isIpV6对不含:的字符串直接返回false,不调用InetAddress.getByName(),避免主机名触发 DNS 解析。
3.3 isIp — 联合判断
IpUtils.isIp("192.168.1.1"); // true — IPv4
IpUtils.isIp("::1"); // true — IPv6
IpUtils.isIp("not-an-ip"); // false
四、地址分类
4.1 私有/公网地址
// ========== 私有地址(RFC 1918) ==========
IpUtils.isPrivateIp("10.0.0.1"); // true — 10.x
IpUtils.isPrivateIp("172.16.0.1"); // true — 172.16-31.x
IpUtils.isPrivateIp("192.168.1.1"); // true — 192.168.x
IpUtils.isPrivateIp("8.8.8.8"); // false — 公网 IP
// ========== 公网可路由地址 ==========
IpUtils.isPublicIp("8.8.8.8"); // true — Google DNS
IpUtils.isPublicIp("1.1.1.1"); // true — Cloudflare DNS
IpUtils.isPublicIp("10.0.0.1"); // false — 私有
IpUtils.isPublicIp("127.0.0.1"); // false — 回环
IpUtils.isPublicIp("169.254.1.1"); // false — 链路本地
IpUtils.isPublicIp("0.0.0.0"); // false — "This"网络
IpUtils.isPublicIp("224.0.0.1"); // false — 组播
IpUtils.isPublicIp("255.255.255.255"); // false — 广播
4.2 特殊地址
// 回环地址
IpUtils.isLoopbackAddress("127.0.0.1"); // true
IpUtils.isLoopbackAddress("::1"); // true — IPv6 回环
IpUtils.isLoopbackAddress("localhost"); // false — DNS 注入防御
// 链路本地
IpUtils.isLinkLocal("169.254.1.1"); // true — IPv4 链路本地
IpUtils.isLinkLocal("fe80::1"); // true — IPv6 链路本地
// 组播地址
IpUtils.isMulticast("224.0.0.1"); // true
IpUtils.isMulticast("239.255.255.255"); // true
// 广播地址
IpUtils.isBroadcast("255.255.255.255"); // true
// IANA 保留(RFC 5737 测试网络 + RFC 6598 运营商级 NAT)
IpUtils.isReserved("192.0.2.1"); // true — TEST-NET-1
IpUtils.isReserved("100.64.0.1"); // true — 运营商级 NAT
// IP 地址分类(A/B/C/D/E)
IpUtils.getIpClass("10.0.0.1"); // 'A'
IpUtils.getIpClass("192.168.1.1"); // 'C'
IpUtils.getIpClass("224.0.0.1"); // 'D' — 组播
五、数值转换
5.1 IP ↔ long 互转
常用于 IP 地理位置数据库查询、IP 范围比较:
// IP → long
long ipLong = IpUtils.toLongByIp("192.168.1.1"); // 3232235777
IpUtils.toLongByIp("invalid"); // -1 — 非法 IP
// long → IP
String ip = IpUtils.toIpByLong(3232235777L); // "192.168.1.1"
5.2 IP ↔ byte[] 互转
常用于网络协议处理:
// IP → byte[4]
byte[] bytes = IpUtils.toBytes("192.168.1.1");
// bytes = {(byte)192, (byte)168, 1, 1}
// byte[4] → IP
String ip = IpUtils.fromBytes(new byte[]{(byte)192, (byte)168, 1, 1});
// "192.168.1.1"
5.3 子网掩码 ↔ CIDR 互转
// 掩码 → CIDR 前缀长度
IpUtils.maskToCidr("255.255.255.0"); // 24
IpUtils.maskToCidr("255.255.0.0"); // 16
IpUtils.maskToCidr("255.0.255.0"); // -1 — 非法掩码(非连续 1)
// CIDR 前缀长度 → 掩码
IpUtils.cidrToMask(24); // "255.255.255.0"
IpUtils.cidrToMask(16); // "255.255.0.0"
IpUtils.cidrToMask(33); // null — 超范围
5.4 网络地址与广播地址
// 计算网络地址
IpUtils.getNetworkAddress("192.168.1.100", "255.255.255.0"); // "192.168.1.0"
IpUtils.getNetworkAddress("10.0.5.37", "255.255.0.0"); // "10.0.0.0"
// 计算广播地址
IpUtils.getBroadcastAddress("192.168.1.100", "255.255.255.0"); // "192.168.1.255"
// 子网可用主机数
IpUtils.getAvailableHostCount("255.255.255.0"); // 254(/24)
IpUtils.getAvailableHostCount("255.255.255.252"); // 2(/30)
IpUtils.getAvailableHostCount("255.255.255.254"); // 2(/31,点对点链路 RFC 3021)
IpUtils.getAvailableHostCount("255.255.255.255"); // 1(/32,单主机)
六、CIDR 子网匹配
6.1 isInSubnet — 单 CIDR 匹配
IpUtils.isInSubnet("192.168.1.5", "192.168.1.0/24"); // true
IpUtils.isInSubnet("192.168.2.5", "192.168.1.0/24"); // false
IpUtils.isInSubnet("10.0.0.1", "10.0.0.0/8"); // true
IpUtils.isInSubnet("10.0.0.1", "0.0.0.0/0"); // true — /0 匹配所有
// 边界情况
IpUtils.isInSubnet("192.168.1.100", "192.168.1.100/32"); // true — 精确匹配
IpUtils.isInSubnet("10.0.0.1", "10.0.0.0/33"); // false — 前缀超范围
6.2 isInSubnet — 多子网匹配(Collection)
// 从数据库/配置加载白名单子网
List<String> subnets = Arrays.asList("10.0.0.0/8", "192.168.1.0/24", "172.16.0.0/12");
IpUtils.isInSubnet("192.168.1.50", subnets); // true — 匹配 192.168.1.0/24
IpUtils.isInSubnet("10.0.0.1", subnets); // true — 匹配 10.0.0.0/8
IpUtils.isInSubnet("8.8.8.8", subnets); // false — 不在任何子网中
// Set 用法
Set<String> allowedSubnets = new HashSet<>(Arrays.asList("10.0.0.0/8", "192.168.0.0/16"));
IpUtils.isInSubnet("10.0.0.1", allowedSubnets); // true
6.3 isInRange — IP 范围检查
IpUtils.isInRange("192.168.1.50", "192.168.1.1", "192.168.1.100"); // true
IpUtils.isInRange("192.168.1.1", "192.168.1.1", "192.168.1.100"); // true — 含边界
IpUtils.isInRange("192.168.1.101", "192.168.1.1", "192.168.1.100"); // false
// start > end 时返回 false
IpUtils.isInRange("192.168.1.50", "192.168.1.100", "192.168.1.1"); // false
七、IP 段解析(四种格式统一处理)
IpUtils 的 IP 段方法统一支持四种格式:
| 格式 | 示例 | 说明 |
|---|---|---|
| CIDR | 192.168.1.0/24 |
标准 CIDR 表示法 |
| 范围 | 192.168.1.1-192.168.1.100 |
start-end |
| 通配符 | 192.168.1.* |
* 代表 0-255 |
| 单 IP | 192.168.1.1 |
精确匹配 |
7.1 matchIpRange — 自动识别格式
// CIDR
IpUtils.matchIpRange("192.168.1.5", "192.168.1.0/24"); // true
// 通配符
IpUtils.matchIpRange("192.168.1.50", "192.168.1.*"); // true
// 范围
IpUtils.matchIpRange("192.168.1.50", "192.168.1.1-192.168.1.100"); // true
// 单 IP
IpUtils.matchIpRange("192.168.1.1", "192.168.1.1"); // true
7.2 countIpRange — IP 段内数量
IpUtils.countIpRange("192.168.1.0/24"); // 256
IpUtils.countIpRange("10.0.0.0/8"); // 16777216
IpUtils.countIpRange("192.168.1.*"); // 256
IpUtils.countIpRange("192.168.1.1-192.168.1.100"); // 100
7.3 listIpRange — 枚举 IP(上限 65536,防 OOM)
List<String> ips = IpUtils.listIpRange("192.168.1.0/30");
// ["192.168.1.0", "192.168.1.1", "192.168.1.2", "192.168.1.3"]
List<String> ips2 = IpUtils.listIpRange("10.0.0.1-10.0.0.5");
// ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4", "10.0.0.5"]
// 超限返回空列表(防 OOM)
List<String> empty = IpUtils.listIpRange("10.0.0.0/15"); // 131072 > 65536
// []
八、批量匹配(白名单/黑名单)
8.1 matchAny / matchNone — 可变参数
// 白名单:IP 是否属于列表中任意一个
boolean inWhitelist = IpUtils.matchAny("192.168.1.5",
"10.0.0.0/8",
"192.168.1.0/24",
"172.16.0.1"
); // true — 匹配 192.168.1.0/24
// 混合格式支持
IpUtils.matchAny("192.168.1.50",
"10.0.0.0/8", // CIDR
"192.168.1.*", // 通配符
"172.16.0.1-172.16.0.100" // 范围
); // true — 匹配通配符
8.2 matchAny / matchNone — Collection(推荐)
// 从数据库/配置加载白名单
List<String> dbWhitelist = ipWhitelistRepository.findAllCidrs();
if (IpUtils.matchAny(clientIp, dbWhitelist)) {
// 在白名单中,放行
}
// Set 用法(推荐,去重 + O(1) 查找)
Set<String> blacklist = new HashSet<>(Arrays.asList(
"10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"
));
boolean isBlocked = !IpUtils.matchNone("192.168.1.5", blacklist); // true — 在黑名单中
九、实战:IP 白名单拦截器
@Component
public class IpWhitelistInterceptor implements HandlerInterceptor {
// 从配置加载白名单
@Value("${ip.whitelist:10.0.0.0/8,192.168.0.0/16}")
private String whitelistConfig;
private Set<String> whitelist;
@PostConstruct
public void init() {
whitelist = new HashSet<>(Arrays.asList(whitelistConfig.split(",")));
}
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String clientIp = getClientIp(request);
// 一行判断是否在白名单中
if (!IpUtils.matchAny(clientIp, whitelist)) {
response.setStatus(403);
response.getWriter().write("{\"code\":403,\"msg\":\"IP不在白名单中\"}");
return false;
}
return true;
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
} else {
ip = ip.split(",")[0].trim();
}
return ip;
}
}
十、IDC 探测(SSRF 加固)
10.1 isPortOpen — 端口连通性检测
// 默认超时 5000ms
boolean open = IpUtils.isPortOpen("8.8.8.8", 53);
// 自定义超时
boolean open2 = IpUtils.isPortOpen("192.168.1.1", 80, 3000);
// SSRF 防御 — 主机名被拒绝
IpUtils.isPortOpen("localhost", 80); // false — 非 IP 格式
IpUtils.isPortOpen("google.com", 443); // false — 非 IP 格式
安全设计:
isPortOpen强制要求合法 IPv4 格式,拒绝主机名,防止攻击者通过传入主机名进行内网探测。
10.2 measureLatency — TCP 连接延迟
// 默认超时 5000ms
long latency = IpUtils.measureLatency("8.8.8.8", 53); // 如 15(毫秒)
// 连接失败返回 -1
long failed = IpUtils.measureLatency("192.0.2.1", 80, 1000); // -1
10.3 getReverseDns — PTR 反向 DNS
String hostname = IpUtils.getReverseDns("8.8.8.8"); // 如 "dns.google"
IpUtils.getReverseDns("192.168.1.1"); // null — 无 PTR 记录
// DNS 放大攻击防御
IpUtils.getReverseDns("localhost"); // null — 非 IP
IpUtils.getReverseDns("evil.com"); // null — 非 IP
10.4 实战:服务健康检查
@Service
public class ServiceHealthChecker {
public List<ServiceStatus> checkAll(List<ServiceEndpoint> endpoints) {
return endpoints.parallelStream().map(ep -> {
boolean portOpen = IpUtils.isPortOpen(ep.getIp(), ep.getPort(), 3000);
long latency = IpUtils.measureLatency(ep.getIp(), ep.getPort(), 3000);
return ServiceStatus.builder()
.name(ep.getName())
.ip(ep.getIp())
.port(ep.getPort())
.alive(portOpen)
.latencyMs(latency)
.build();
}).collect(Collectors.toList());
}
}
十一、IP 脱敏
日志展示、接口返回中的 IP 需要脱敏处理:
// 可配置保留段数
IpUtils.hideIp("192.168.1.100", 3); // "192.168.1.***"
IpUtils.hideIp("192.168.1.100", 2); // "192.168.***.***"
IpUtils.hideIp("192.168.1.100", 1); // "192.***.***.***"
// 快捷脱敏(保留前 3 段)
IpUtils.hideLocalIp(); // 如 "192.168.1.***"
IpUtils.hideOutIp("10.0.0.5"); // "10.0.0.***"
实战:日志脱敏
// 在日志输出前脱敏
logger.info("用户 {} 从 IP {} 登录", username, IpUtils.hideOutIp(clientIp));
// 输出:用户 admin 从 IP 192.168.1.*** 登录
十二、本机网络信息
// 获取主机名
String hostname = IpUtils.getLocalHostName(); // 如 "myhost"
// 获取本机 IP
String ip = IpUtils.getLocalAddress(); // 如 "192.168.1.100"
// 获取所有 IPv4 地址(多网卡场景)
List<String> allIps = IpUtils.getIpV4AllAddress();
// ["127.0.0.1", "192.168.1.100", "10.0.0.5"]
// 获取有效业务 IP(过滤回环和链路本地)
List<String> effectiveIps = IpUtils.getEffectiveAddresses();
// ["192.168.1.100", "10.0.0.5"] — 不含 127.0.0.1
// 按前缀匹配本机 IP(多网卡场景)
String dockerIp = IpUtils.prefixIpv4LocalAddress("172.17"); // Docker 网卡
String lanIp = IpUtils.prefixIpv4LocalAddress("192.168"); // 局域网网卡
// 获取 MAC 地址
String mac = IpUtils.getMacAddress("192.168.1.100"); // 如 "AA:BB:CC:DD:EE:FF"
// 获取机器码(基于网卡哈希,用于软件授权)
int machineCode = IpUtils.getMachine();
十三、安全特性汇总
IpUtils 内置 13 项安全加固,全部默认开启:
| 安全特性 | 防御目标 | 涉及方法 |
|---|---|---|
| DNS 主机名注入防御 | 防止 localhost/evil.com 触发 DNS 解析 |
isIpV6, isLoopbackAddress, isLinkLocal, isSiteLocal, isIpV6Loopback |
| SSRF 防御 | 防止内网探测(如 169.254.169.254 云元数据) |
isPortOpen, measureLatency |
| DNS 放大攻击防御 | 防止主机名注入导致 DNS 放大 | getReverseDns |
| 整数溢出保护 | CIDR 掩码计算使用 long 算术 |
maskToCidr, getNetworkAddress, getBroadcastAddress |
| 前导零拒绝 | 防止八进制解析歧义 | parseSegments(所有 IP 校验的内核) |
| OOM 防护 | listIpRange 上限 65536 |
listIpRange |
| Socket 资源管理 | try-with-resources 确保关闭 | isPortOpen, measureLatency |
十四、API 速查表
格式校验
| 方法 | 说明 |
|---|---|
isIpV4(String) |
严格 IPv4 校验(含前导零拒绝) |
isIpV6(String) |
IPv6 校验(含 DNS 注入防御) |
isIp(String) |
IPv4 或 IPv6 联合判断 |
地址分类
| 方法 | 说明 |
|---|---|
isPrivateIp(String) |
RFC 1918 私有地址 |
isPublicIp(String) / isRoutable(String) |
公网可路由地址 |
isLoopbackAddress(String) |
回环地址(IPv4 + IPv6) |
isLinkLocal(String) |
链路本地 |
isMulticast(String) |
组播地址 |
isBroadcast(String) |
广播地址 |
isReserved(String) |
IANA 保留地址 |
getIpClass(String) |
A/B/C/D/E 分类 |
数值转换
| 方法 | 说明 |
|---|---|
toLongByIp(String) / toIpByLong(long) |
IP ↔ long |
toBytes(String) / fromBytes(byte[]) |
IP ↔ byte[4] |
maskToCidr(String) / cidrToMask(int) |
掩码 ↔ CIDR |
getNetworkAddress(String, String) |
计算网络地址 |
getBroadcastAddress(String, String) |
计算广播地址 |
getAvailableHostCount(String) |
子网可用主机数 |
CIDR 与 IP 段
| 方法 | 说明 |
|---|---|
isInSubnet(String, String) |
单 CIDR 匹配 |
isInSubnet(String, Collection<String>) |
多 CIDR 匹配 |
isInRange(String, String, String) |
IP 范围检查 |
matchIpRange(String, String) |
自动识别格式匹配 |
countIpRange(String) |
IP 段内数量 |
listIpRange(String) |
枚举 IP(上限 65536) |
matchAny(String, String...) |
白名单匹配 |
matchNone(String, String...) |
黑名单匹配 |
IDC 探测
| 方法 | 说明 |
|---|---|
isPortOpen(String, int) |
TCP 端口连通性(SSRF 加固) |
measureLatency(String, int) |
TCP 连接延迟(SSRF 加固) |
getReverseDns(String) |
PTR 反向 DNS(DNS 放大防御) |
getMacAddress(String) |
MAC 地址查询 |
getEffectiveAddresses() |
有效业务 IP |
脱敏与本机信息
| 方法 | 说明 |
|---|---|
hideIp(String, int) |
可配置保留段数脱敏 |
hideLocalIp() / hideOutIp(String) |
快捷脱敏 |
getLocalAddress() |
本机 IP |
getIpV4AllAddress() |
所有 IPv4 地址 |
prefixIpv4LocalAddress(String) |
按前缀匹配本机 IP |
getMachine() |
机器码 |
十五、注意事项
| 问题 | 说明 |
|---|---|
| isIpV6 不接受主机名 | 不含 : 的字符串直接返回 false,不触发 DNS |
| isPortOpen 不接受主机名 | SSRF 防御,强制合法 IPv4 格式 |
| listIpRange 上限 65536 | 防 OOM,更大范围用 isInSubnet 按需匹配 |
| toLongByIp 返回 -1 | 非法 IP 返回哨兵值,不抛异常 |
| getAvailableHostCount /31 返回 2 | RFC 3021 点对点链路,/31 两个 IP 均可用 |
| 零外部依赖 | 仅使用 JDK 标准库(java.net, java.io) |
十六、总结
| 维度 | 手写 IP 工具 | IpUtils |
|---|---|---|
| 校验严格性 | 正则校验,前导零通过 | 内置 13 项安全加固 |
| IPv6 支持 | 通常只支持 IPv4 | 完整 IPv6(含 Zone ID) |
| CIDR 计算 | 每次翻文档手写 | 一行调用 |
| 白名单/黑名单 | 遍历 List 逐个比 | matchAny/matchNone 批量匹配 |
| 安全性 | 无 SSRF 防御 | 内置 SSRF/DNS 注入防御 |
| 依赖 | 可能依赖 Guava 等 | 零外部依赖 |
源码地址:pan-common
如果这篇文章对你有帮助,欢迎点赞、收藏、关注,后续将持续更新 Java 工具类实战系列。
更多推荐
所有评论(0)