ReqIpUtil.java 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. package com.jtzx.crm.module.api.util;
  2. import cc.uncarbon.framework.core.context.UserContextHolder;
  3. import cc.uncarbon.framework.web.util.IPUtil;
  4. import cn.hutool.core.collection.CollUtil;
  5. import cn.hutool.core.text.CharSequenceUtil;
  6. import jakarta.servlet.http.HttpServletRequest;
  7. import java.util.List;
  8. public class ReqIpUtil {
  9. public static String getIpAddr(HttpServletRequest request) {
  10. String ip = UserContextHolder.getClientIP();
  11. // 2. 如果为空,或者拿到的明显是本地内网回环,则用 IPUtil 兜底获取
  12. if (CharSequenceUtil.isEmpty(ip) || isLoopbackOrInternal(ip)) {
  13. // 先尝试通过 IPUtil 获取整个未分割的 IP 串(避免因为固定的 index=0 错失公网IP)
  14. // 注意:因为工具类把 REMOTE_ADDR 排在 X-Real-IP 前面,我们先手动夹带一下核心请求头
  15. ip = request.getHeader("X-Forwarded-For");
  16. if (CharSequenceUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
  17. ip = request.getHeader("X-Real-IP");
  18. }
  19. // 如果上面两个核心头都没拿到,再让 IPUtil 去遍历它那一堆头
  20. if (CharSequenceUtil.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
  21. ip = IPUtil.getClientIPAddress(request, 0);
  22. }
  23. }
  24. // 3. 【核心修复】深度解析获取到的 IP,确保拿到的是公网 IP
  25. if (CharSequenceUtil.isNotEmpty(ip)) {
  26. // 如果包含逗号(说明是多层代理返回的 IP 链)
  27. if (CharSequenceUtil.contains(ip, ",")) {
  28. List<String> ipList = CharSequenceUtil.split(ip, ",");
  29. // 遍历 IP 链,找到第一个真正的公网 IP
  30. String publicIp = null;
  31. for (String singleIp : ipList) {
  32. String cleanIp = CharSequenceUtil.cleanBlank(singleIp);
  33. if (CharSequenceUtil.isNotEmpty(cleanIp) && !isLoopbackOrInternal(cleanIp) && !"unknown".equalsIgnoreCase(cleanIp)) {
  34. publicIp = cleanIp;
  35. break; // 找到第一个外网 IP,跳出循环
  36. }
  37. }
  38. // 如果找到了公网 IP 就用它,否则降级取原本的第一个
  39. if (publicIp != null) {
  40. ip = publicIp;
  41. } else {
  42. ip = CharSequenceUtil.cleanBlank(CollUtil.get(ipList, 0));
  43. }
  44. } else if (isLoopbackOrInternal(ip)) {
  45. // 如果不是逗号分隔,但本身是个内网 IP,且代理传了 X-Real-IP,做最后一次尝试
  46. String realIp = request.getHeader("X-Real-IP");
  47. if (CharSequenceUtil.isNotEmpty(realIp) && !isLoopbackOrInternal(realIp) && !"unknown".equalsIgnoreCase(realIp)) {
  48. ip = realIp.trim();
  49. }
  50. }
  51. }
  52. return ip;
  53. }
  54. /**
  55. * 辅助方法:判断 IP 是否为回环地址或局域网内网地址
  56. */
  57. private static boolean isLoopbackOrInternal(String ip) {
  58. if (ip == null) return true;
  59. String cleanIp = ip.trim();
  60. if ("127.0.0.1".equals(cleanIp) || "0:0:0:0:0:0:0:1".equals(cleanIp) || "localhost".equalsIgnoreCase(cleanIp)) {
  61. return true;
  62. }
  63. // 匹配内网 A/B/C 类私有地址段:10.x.x.x, 192.168.x.x, 172.16.x.x - 172.31.x.x
  64. return cleanIp.startsWith("10.") || cleanIp.startsWith("192.168.") ||
  65. (cleanIp.startsWith("172.") && cleanIp.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"));
  66. }
  67. // 辅助方法:判断是否为内网 IP(10.x, 172.16.x-172.31.x, 192.168.x)
  68. private static boolean isInternalIp(String ip) {
  69. if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) return true;
  70. // 简单正则匹配常见的私有内网 IP 段
  71. return ip.startsWith("10.") || ip.startsWith("192.168.") ||
  72. (ip.startsWith("172.") && ip.matches("^172\\.(1[6-9]|2[0-9]|3[0-1])\\..*"));
  73. }
  74. }