Просмотр исходного кода

fix: risk服务增加IP白名单跳过校验全局风控322

hidewnd 2 месяцев назад
Родитель
Сommit
2e23aa6191

+ 137 - 0
yt-common/src/main/java/com/ytpm/util/IPUtil.java

@@ -1,7 +1,12 @@
 package com.ytpm.util;
 
 
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.StrUtil;
+
 import javax.servlet.http.HttpServletRequest;
+import java.util.List;
+import java.util.regex.Pattern;
 
 /**
  * @author lih
@@ -16,4 +21,136 @@ public class IPUtil {
         }
         return xfHeader.split(",")[0]; // 可能会有多个IP,这里取第一个逗号前的IP
     }
+
+
+    // 严格的CIDR正则表达式(支持 x.x.x.x/x 格式)
+    private static final Pattern CIDR_PATTERN = Pattern.compile(
+            "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)/([0-9]|[1-2]\\d|3[0-2])$"
+    );
+
+    // 严格的IPv4正则表达式(禁止前导零)
+    private static final Pattern IPV4_PATTERN = Pattern.compile(
+            "^((25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)\\.){3}(25[0-5]|2[0-4]\\d|1\\d{2}|[1-9]?\\d)$"
+    );
+
+    /**
+     * 校验IP是否在白名单中(支持单个IP和CIDR网段)
+     * @param ip 待校验的IP地址
+     * @param whitelist 白名单列表,支持IPv4和CIDR格式
+     * @return 是否在白名单中
+     */
+    public static boolean isIpInWhitelist(String ip, List<String> whitelist) {
+        // 参数校验
+        if (ip == null || ip.trim().isEmpty() || whitelist == null || whitelist.isEmpty()) {
+            return false;
+        }
+
+        ip = ip.trim();
+        // 预校验IP格式
+        if (!IPV4_PATTERN.matcher(ip).matches()) {
+            return false;
+        }
+
+        // 遍历白名单进行匹配
+        for (String item : whitelist) {
+            if (item == null) {
+                continue;
+            }
+
+            String trimmedItem = item.trim();
+            if (trimmedItem.isEmpty()) {
+                continue;
+            }
+
+            // 优先检查精确IP匹配
+            if (IPV4_PATTERN.matcher(trimmedItem).matches()) {
+                if (ip.equals(trimmedItem)) {
+                    return true;
+                }
+                continue;
+            }
+
+            // 检查CIDR网段匹配
+            if (CIDR_PATTERN.matcher(trimmedItem).matches()) {
+                try {
+                    if (isIpInCidr(ip, trimmedItem)) {
+                        return true;
+                    }
+                } catch (IllegalArgumentException e) {
+                    // 记录日志或忽略无效的CIDR条目
+                    continue;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 校验IP是否在CIDR网段内
+     * 例:IP=192.168.1.100,CIDR=192.168.1.0/24 → 返回 true
+     * @param ip IPv4地址
+     * @param cidr CIDR网段格式
+     * @return 是否在网段内
+     * @throws IllegalArgumentException 参数格式无效时抛出
+     */
+    public static boolean isIpInCidr(String ip, String cidr) {
+        if (ip == null || cidr == null) {
+            throw new IllegalArgumentException("IP和CIDR不能为null");
+        }
+
+        // 使用预编译的正则进行格式校验
+        if (!CIDR_PATTERN.matcher(cidr).matches()) {
+            throw new IllegalArgumentException("无效的CIDR网段格式:" + cidr);
+        }
+
+        if (!IPV4_PATTERN.matcher(ip).matches()) {
+            throw new IllegalArgumentException("无效的IPv4地址:" + ip);
+        }
+
+        String[] cidrParts = cidr.split("/");
+        String networkIp = cidrParts[0];
+        int prefixLength = Integer.parseInt(cidrParts[1]);
+
+        // 校验掩码长度范围
+        if (prefixLength < 0 || prefixLength > 32) {
+            throw new IllegalArgumentException("CIDR掩码长度必须在0-32之间:" + prefixLength);
+        }
+
+        // 计算子网掩码(处理32位边界情况)
+        long subnetMask = prefixLength == 0 ? 0 : (0xFFFFFFFFL << (32 - prefixLength));
+        // 计算网络地址
+        long ipLong = ipToLong(ip);
+        long networkIpLong = ipToLong(networkIp);
+
+        // 比较网络地址
+        return (ipLong & subnetMask) == (networkIpLong & subnetMask);
+    }
+
+    /**
+     * IPv4地址转长整型
+     * 例:192.168.1.1 → 3232235777
+     * @param ip IPv4地址字符串
+     * @return 对应的长整型数值
+     * @throws IllegalArgumentException IP格式无效时抛出
+     */
+    public static long ipToLong(String ip) {
+        if (ip == null) {
+            throw new IllegalArgumentException("IP地址不能为null");
+        }
+
+        if (!IPV4_PATTERN.matcher(ip).matches()) {
+            throw new IllegalArgumentException("无效的IPv4地址:" + ip);
+        }
+
+        String[] parts = ip.split("\\.");
+        long result = 0;
+        for (int i = 0; i < 4; i++) {
+            int segment = Integer.parseInt(parts[i]);
+            if (segment < 0 || segment > 255) {
+                throw new IllegalArgumentException("IP地址段超出范围:" + segment);
+            }
+            result |= (segment & 0xFFL) << (8 * (3 - i));
+        }
+        return result;
+    }
 }

+ 43 - 0
yt-risk/risk-manage/src/main/java/com/ytpm/config/RedisCacheInitializer.java

@@ -0,0 +1,43 @@
+package com.ytpm.config;
+
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.ytpm.util.RedisService;
+import com.ytpm.util.ResourceUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * @author lih
+ * @date 2025-11-18 11:09
+ */
+@Slf4j
+@Component
+public class RedisCacheInitializer {
+
+    @Resource
+    private RedisService redisService;
+
+    @Value("${spring.config.white-ip.file-name:WhiteIpList.txt}")
+    private String configFileName;
+
+    @EventListener(ApplicationReadyEvent.class)
+    public void initRedisCache() {
+        try {
+            List<String> strings = ResourceUtils.readFileToList(configFileName);
+            if (CollectionUtils.isNotEmpty(strings)) {
+                String cacheKey = "risk:whiteIps";
+                redisService.setStr(cacheKey, String.join(",", strings));
+                log.info("已加载IP白名单:{}", String.join(",", strings));
+            }
+        } catch (IOException ignored) {}
+    }
+}

+ 21 - 0
yt-risk/risk-manage/src/main/java/com/ytpm/service/impl/RiskServiceImpl.java

@@ -2,6 +2,7 @@ package com.ytpm.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.ConcurrentHashSet;
 import cn.hutool.core.date.DateField;
 import cn.hutool.core.date.DateUnit;
 import cn.hutool.core.date.DateUtil;
@@ -12,6 +13,7 @@ import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
 import com.ytpm.advertise.enums.AdSourceTypeEnum;
@@ -57,7 +59,9 @@ import com.ytpm.risk.view.RiskDeblockingListView;
 import com.ytpm.risk.view.RiskTemplateView;
 import com.ytpm.service.RiskService;
 import com.ytpm.util.FeignClientInvoker;
+import com.ytpm.util.IPUtil;
 import com.ytpm.util.RedisService;
+import com.ytpm.util.ResourceUtils;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -67,6 +71,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
+import java.io.IOException;
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -83,6 +88,8 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.CopyOnWriteArraySet;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
@@ -121,6 +128,8 @@ public class RiskServiceImpl implements RiskService {
     private ScheduledExecutorService scheduledExecutorService;
 
 
+    private static Set<String> ipWhiteList = new CopyOnWriteArraySet<>();
+
     /**
      * 查询配置字段选项
      */
@@ -417,6 +426,10 @@ public class RiskServiceImpl implements RiskService {
      * 校验风控规则322
      */
     private void checkRisk322(YtDyzUser dyzUser) {
+        // ip白名单用户直接放行
+        if (checkDyzUserIp(dyzUser)) {
+            return;
+        }
         RiskTemplateView view = configMapper.getByCode("322");
         //根据用户所属应用查询该应用母包openid查询用户信息
         YtApp ytApp = appMapper.selectRiskApp(dyzUser.getAppId());
@@ -448,6 +461,14 @@ public class RiskServiceImpl implements RiskService {
         riskLockUser(dyzUser, "322", "系统判定重复刷单用户限制", tips);
     }
 
+    private boolean checkDyzUserIp(YtDyzUser dyzUser) {
+        String cacheKey = "risk:whiteIps";
+        if (!redisService.hasKey(cacheKey) || StrUtil.isEmpty(dyzUser.getLastLoginIp())) {
+            return false;
+        }
+        return IPUtil.isIpInWhitelist(dyzUser.getLastLoginIp(), Arrays.asList(redisService.getStr(cacheKey).split(",")));
+    }
+
     /**
      * 校验默认风控配置, 固定配置值不改
      *  days ?天内为1