Browse Source

New: 新增JA4指纹生成器,融合前后端指纹生成

lwh 6 days ago
parent
commit
c1d0521cdd

+ 6 - 0
pig-marketing/pig-marketing-api/pom.xml

@@ -51,5 +51,11 @@
 			<groupId>com.pig4cloud</groupId>
 			<artifactId>pig-common-excel</artifactId>
 		</dependency>
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>javax.servlet-api</artifactId>
+			<version>4.0.1</version>
+			<scope>compile</scope>
+		</dependency>
 	</dependencies>
 </project>

+ 71 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/FingerprintCombinerUtil.java

@@ -0,0 +1,71 @@
+package com.pig4cloud.pig.marketing.api.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 指纹组合工具类
+ */
+public class FingerprintCombinerUtil {
+
+    /**
+     * 组合前端指纹和后端指纹
+     * @param frontFingerprint 前端传入的指纹(可为null)
+     * @param backendFingerprint 后端生成的指纹(可为null)
+     * @return 组合后的唯一指纹(32位MD5或64位SHA-256)
+     */
+    public static String combine(String frontFingerprint, String backendFingerprint) {
+        // 处理空值(空字符串替换为"null",避免拼接后差异丢失)
+        String front = (frontFingerprint == null || frontFingerprint.trim().isEmpty()) 
+            ? "null" : frontFingerprint.trim();
+        String backend = (backendFingerprint == null || backendFingerprint.trim().isEmpty()) 
+            ? "null" : backendFingerprint.trim();
+        
+        // 固定顺序拼接(前端+分隔符+后端)
+        String combined = front + "|" + backend;
+        
+        // 生成SHA-256哈希(推荐,比MD5更安全)
+        return generateSha256(combined);
+//		return generateMd5(combined);
+    }
+
+    /**
+     * 生成SHA-256哈希
+     */
+    private static String generateSha256(String input) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("SHA-256");
+            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+            
+            // 转换为16进制字符串
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hash) {
+                String hex = String.format("%02x", b);
+                hexString.append(hex);
+            }
+            return hexString.toString();
+        } catch (NoSuchAlgorithmException e) {
+            // 异常时降级为MD5(理论上SHA-256是JDK标配,此处为容错)
+            return generateMd5(input);
+        }
+    }
+
+    /**
+     * 生成MD5哈希(降级方案)
+     */
+    private static String generateMd5(String input) {
+        try {
+            MessageDigest digest = MessageDigest.getInstance("MD5");
+            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+            
+            StringBuilder hexString = new StringBuilder();
+            for (byte b : hash) {
+                hexString.append(String.format("%02x", b));
+            }
+            return hexString.toString();
+        } catch (NoSuchAlgorithmException e) {
+			return input.length() >= 32 ? input.substring(0, 32) : input;
+        }
+    }
+}

+ 281 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/JA4GeneratorUtil.java

@@ -0,0 +1,281 @@
+package com.pig4cloud.pig.marketing.api.util;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.net.ssl.SSLSession;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * JA4指纹生成器(整合TLS会话获取功能)
+ * 负责HTTPS请求的JA4指纹生成,内部包含从请求中提取SSL会话的逻辑
+ */
+@Slf4j
+public class JA4GeneratorUtil {
+
+	// 密码套件映射表(可根据实际需求扩展)
+	private static final Map<String, String> CIPHER_SUITE_MAPPINGS = new HashMap<>() {{
+		put("TLS_AES_128_GCM_SHA256", "a128gcm");
+		put("TLS_AES_256_GCM_SHA384", "a256gcm");
+		put("TLS_CHACHA20_POLY1305_SHA256", "chacha20");
+		put("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "e128gcm");
+		put("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "r128gcm");
+		put("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "e256gcm");
+		put("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "r256gcm");
+		put("TLS_RSA_WITH_AES_128_GCM_SHA256", "rsa128gcm");
+		put("TLS_RSA_WITH_AES_256_GCM_SHA384", "rsa256gcm");
+	}};
+
+	// 扩展类型映射(简化版)
+	private static final Map<Integer, String> EXTENSION_MAPPINGS = new HashMap<>() {{
+		put(0, "sni");           // 服务器名称指示
+		put(10, "ec_points");    // 椭圆曲线点格式
+		put(16, "alpn");         // 应用层协议协商
+		put(18, "ec_points");    // 椭圆曲线
+		put(21, "status_req");   // OCSP状态请求
+		put(43, "ec_points");    // 加密套件
+		put(44, "ec_points");    // 压缩方法
+		put(51, "renegotiate");  // 重新协商
+		put(65281, "grease");    // GREASE
+	}};
+
+	/**
+	 * 从HttpServletRequest中获取SSLSession
+	 * 整合原TlsUtils的功能
+	 */
+	public SSLSession getSslSession(HttpServletRequest request) {
+		if (request == null) {
+			return null;
+		}
+
+		try {
+			// 尝试直接从请求属性获取(兼容Tomcat等容器)
+			Object sslSession = request.getAttribute("javax.servlet.request.ssl_session");
+			if (sslSession instanceof SSLSession) {
+				return (SSLSession) sslSession;
+			}
+
+			// 反射方式获取(兼容不同容器)
+			Method getSessionMethod = request.getClass().getMethod("getSession");
+			Object session = getSessionMethod.invoke(request);
+			if (session instanceof SSLSession) {
+				return (SSLSession) session;
+			}
+
+			// 尝试获取SSL会话的其他方法
+			Method getSslSessionMethod = request.getClass().getMethod("getSslSession");
+			Object result = getSslSessionMethod.invoke(request);
+			if (result instanceof SSLSession) {
+				return (SSLSession) result;
+			}
+		} catch (Exception e) {
+			// 获取失败,可能不是HTTPS请求或容器不支持
+			log.error("Failed to get SSL session", e);
+		}
+
+		return null;
+	}
+
+	/**
+	 * 生成JA4指纹
+	 * @param request HTTP请求对象(需为HTTPS请求)
+	 * @return JA4指纹字符串,非HTTPS请求或生成失败时返回null
+	 */
+	public String generateJA4(HttpServletRequest request) {
+		try {
+			SSLSession sslSession = getSslSession(request);
+			if (sslSession == null) {
+				return null; // 非HTTPS请求,无法生成指纹
+			}
+			return generateJA4(sslSession);
+		} catch (Exception e) {
+			log.error("Failed to generate JA4 fingerprint:{}",e.getMessage());
+			return null;
+		}
+	}
+
+	/**
+	 * 生成JA4指纹
+	 * @param sslSession SSL会话对象
+	 * @return JA4指纹字符串
+	 */
+	public String generateJA4(SSLSession sslSession) {
+		try {
+			// 1. 提取TLS版本
+			String tlsVersion = getNormalizedTlsVersion(sslSession.getProtocol());
+
+			// 2. 提取密码套件
+			String cipherSuite = getNormalizedCipherSuite(sslSession.getCipherSuite());
+
+			// 3. 提取扩展信息(简化实现,实际需解析握手包)
+			String extensions = getNormalizedExtensions(sslSession);
+
+			// 4. 组合生成JA4指纹
+			String rawJA4 = String.format("%s,%s,%s", tlsVersion, cipherSuite, extensions);
+
+			// 5. 计算哈希值作为最终指纹
+			return computeMD5(rawJA4);
+		} catch (Exception e) {
+			log.error("生成JA4指纹失败:{}", e.getMessage());
+			return null;
+		}
+	}
+
+	/**
+	 * 规范化TLS版本
+	 */
+	private String getNormalizedTlsVersion(String protocol) {
+		if (protocol == null) {
+			return "unknown";
+		}
+		return switch (protocol) {
+			case "TLSv1.0" -> "1.0";
+			case "TLSv1.1" -> "1.1";
+			case "TLSv1.2" -> "1.2";
+			case "TLSv1.3" -> "1.3";
+			default -> protocol.replace("TLSv", "").replace("SSLv", "");
+		};
+	}
+
+	/**
+	 * 规范化密码套件
+	 */
+	private String getNormalizedCipherSuite(String cipherSuite) {
+		if (cipherSuite == null) {
+			return "unknown";
+		}
+		return CIPHER_SUITE_MAPPINGS.getOrDefault(cipherSuite, hashUnknownValue(cipherSuite));
+	}
+
+	/**
+	 * 规范化TLS扩展信息(基于实际扩展类型提取)
+	 */
+	private String getNormalizedExtensions(SSLSession sslSession) {
+		List<String> extensions = new ArrayList<>();
+
+		try {
+			// 1. 尝试从SSLSession中提取扩展信息(不同JDK实现可能有差异)
+			// 反射获取底层TLS握手信息(以SunJDK的SSLSessionImpl为例)
+			Class<?> sslSessionClass = sslSession.getClass();
+
+			// 2. 提取SNI(服务器名称指示,扩展类型0)
+			if (hasSNIExtension(sslSession, sslSessionClass)) {
+				extensions.add("sni");
+			}
+
+			// 3. 提取ALPN(应用层协议协商,扩展类型16)
+			String alpnProtocol = getALPNProtocol(sslSession, sslSessionClass);
+			if (alpnProtocol != null && !alpnProtocol.isEmpty()) {
+				extensions.add("alpn");
+			}
+
+			// 4. 提取椭圆曲线扩展(EC Points,扩展类型10)
+			if (hasECExtension(sslSession, sslSessionClass)) {
+				extensions.add("ec_points");
+			}
+
+			// 5. 提取GREASE扩展(用于测试兼容性,扩展类型0x0a0a等)
+			if (hasGreaseExtension(sslSession, sslSessionClass)) {
+				extensions.add("grease");
+			}
+
+		} catch (Exception e) {
+			// 提取失败时返回默认扩展组合(不影响主流程)
+			return "sni,alpn,ec_points";
+		}
+
+		// 若未提取到任何扩展,返回默认值
+		return extensions.isEmpty() ? "sni,alpn,ec_points" : String.join(",", extensions);
+	}
+
+	/**
+	 * 判断是否存在SNI扩展(服务器名称指示)
+	 */
+	private boolean hasSNIExtension(SSLSession sslSession, Class<?> sslSessionClass) throws Exception {
+		// SunJDK中,SSLSessionImpl的handshakeSession可能包含扩展信息
+		Field handshakeSessionField = sslSessionClass.getDeclaredField("handshakeSession");
+		handshakeSessionField.setAccessible(true);
+		Object handshakeSession = handshakeSessionField.get(sslSession);
+
+		// 从握手信息中获取SNI主机名(若存在则说明有SNI扩展)
+		Method getPeerHostMethod = handshakeSession.getClass().getMethod("getPeerHost");
+		String peerHost = (String) getPeerHostMethod.invoke(handshakeSession);
+		return peerHost != null && !peerHost.isEmpty();
+	}
+
+	/**
+	 * 获取ALPN协商的协议(如http/1.1、h2等)
+	 */
+	private String getALPNProtocol(SSLSession sslSession, Class<?> sslSessionClass) throws Exception {
+		// 从SSLSession中提取ALPN结果(不同JDK实现方法不同)
+		try {
+			Method getApplicationProtocolMethod = sslSessionClass.getMethod("getApplicationProtocol");
+			return (String) getApplicationProtocolMethod.invoke(sslSession);
+		} catch (NoSuchMethodException e) {
+			// 旧版本JDK可能没有该方法,返回null
+			return null;
+		}
+	}
+
+	/**
+	 * 判断是否存在椭圆曲线扩展
+	 */
+	private boolean hasECExtension(SSLSession sslSession, Class<?> sslSessionClass) throws Exception {
+		// 反射获取TLS握手过程中的椭圆曲线参数
+		Field cipherSuiteField = sslSessionClass.getDeclaredField("cipherSuite");
+		cipherSuiteField.setAccessible(true);
+		String cipherSuite = (String) cipherSuiteField.get(sslSession);
+
+		// 包含ECDHE的密码套件通常依赖椭圆曲线扩展
+		return cipherSuite.contains("ECDHE");
+	}
+
+	/**
+	 * 判断是否存在GREASE扩展(用于兼容性测试的预留扩展)
+	 */
+	private boolean hasGreaseExtension(SSLSession sslSession, Class<?> sslSessionClass) throws Exception {
+		// GREASE扩展类型为0x0a0a、0x1a1a等特殊值,此处简化判断
+		// 实际需解析扩展类型列表,这里通过密码套件特征近似判断
+		String protocol = sslSession.getProtocol();
+		return protocol.startsWith("TLSv1.3") || protocol.startsWith("TLSv1.2");
+	}
+
+	/**
+	 * 对未知值进行哈希处理
+	 */
+	private String hashUnknownValue(String value) {
+		try {
+			MessageDigest md = MessageDigest.getInstance("MD5");
+			byte[] digest = md.digest(value.getBytes(StandardCharsets.UTF_8));
+			StringBuilder hexString = new StringBuilder();
+			for (byte b : digest) {
+				hexString.append(String.format("%02x", b));
+			}
+			return hexString.substring(0, 8); // 取前8位
+		} catch (NoSuchAlgorithmException e) {
+			return value.hashCode() + "";
+		}
+	}
+
+	/**
+	 * 计算MD5哈希
+	 */
+	private String computeMD5(String input) throws NoSuchAlgorithmException {
+		MessageDigest md = MessageDigest.getInstance("MD5");
+		byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
+
+		StringBuilder hexString = new StringBuilder();
+		for (byte b : digest) {
+			hexString.append(String.format("%02x", b));
+		}
+		return hexString.toString();
+	}
+}

+ 173 - 0
pig-marketing/pig-marketing-api/src/main/java/com/pig4cloud/pig/marketing/api/util/JA4HGeneratorUtil.java

@@ -0,0 +1,173 @@
+package com.pig4cloud.pig.marketing.api.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * JA4H指纹生成工具类
+ * 基于HTTP请求的关键特征生成指纹,遵循JA4规范
+ */
+public class JA4HGeneratorUtil {
+
+    // JA4H需要提取的关键HTTP头部(按规范定义的顺序)
+    private static final List<String> KEY_HEADERS = Arrays.asList(
+        "Accept",
+        "Accept-Language",
+        "Accept-Encoding",
+        "User-Agent",
+        "Connection",
+        "Upgrade",
+        "Content-Length",
+        "Content-Type"
+    );
+
+    /**
+     * 生成JA4H指纹
+     * @param method HTTP方法(如GET、POST)
+     * @param path 请求路径(不含查询参数)
+     * @param httpVersion HTTP版本(如HTTP/1.1)
+     * @param headers HTTP请求头部键值对
+     * @return JA4H指纹(MD5哈希值)
+     */
+    public String generateJA4H(String method, String path, String httpVersion, Map<String, String> headers) {
+        try {
+            // 1. 规范化输入参数
+            String normalizedMethod = normalizeMethod(method);
+            String normalizedPath = normalizePath(path);
+            String normalizedVersion = normalizeHttpVersion(httpVersion);
+            
+            // 2. 提取并规范化关键头部
+            List<String> headerValues = extractAndNormalizeHeaders(headers);
+            
+            // 3. 按JA4H规则拼接原始字符串
+            String rawJA4H = buildRawJA4HString(
+                normalizedMethod,
+                normalizedPath,
+                normalizedVersion,
+                headerValues
+            );
+            
+            // 4. 计算MD5哈希作为最终指纹
+            return computeMD5(rawJA4H);
+            
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to generate JA4H fingerprint", e);
+        }
+    }
+
+    /**
+     * 规范化HTTP方法(大写处理)
+     */
+    private String normalizeMethod(String method) {
+        return method == null ? "" : method.trim().toUpperCase();
+    }
+
+    /**
+     * 规范化请求路径(去除查询参数,保留基础路径)
+     */
+    private String normalizePath(String path) {
+        if (path == null || path.trim().isEmpty()) {
+            return "/";
+        }
+        // 移除查询参数(?后面的部分)
+        int queryIndex = path.indexOf('?');
+        return queryIndex != -1 ? path.substring(0, queryIndex).trim() : path.trim();
+    }
+
+    /**
+     * 规范化HTTP版本
+     */
+    private String normalizeHttpVersion(String version) {
+        if (version == null) {
+            return "";
+        }
+        // 提取主版本(如HTTP/1.1 -> 1.1)
+        String[] parts = version.trim().split("/");
+        return parts.length > 1 ? parts[1] : version.trim();
+    }
+
+    /**
+     * 提取并规范化关键头部
+     * 按KEY_HEADERS定义的顺序提取,不存在的头部用空字符串表示
+     */
+    private List<String> extractAndNormalizeHeaders(Map<String, String> headers) {
+        if (headers == null) {
+            headers = Collections.emptyMap();
+        }
+        
+        // 转换为小写键的映射,方便查找
+        Map<String, String> lowerCaseHeaders = headers.entrySet().stream()
+                .collect(Collectors.toMap(
+                    entry -> entry.getKey().toLowerCase(),
+                    entry -> normalizeHeaderValue(entry.getValue()),
+                    (v1, v2) -> v1 // 处理重复头部,取第一个值
+                ));
+        
+        // 按预设顺序提取头部值
+        return KEY_HEADERS.stream()
+                .map(header -> lowerCaseHeaders.getOrDefault(header.toLowerCase(), ""))
+                .collect(Collectors.toList());
+    }
+
+    /**
+     * 规范化头部值(去除首尾空格,多个值用逗号拼接)
+     */
+    private String normalizeHeaderValue(String value) {
+        if (value == null) {
+            return "";
+        }
+        // 处理多个值的情况(如Accept: text/html, application/json)
+        return value.trim().replaceAll("\\s+,\\s+", ",").replaceAll("\\s+", " ");
+    }
+
+    /**
+     * 构建JA4H原始字符串
+     * 格式:method,path,version,header1,header2,...
+     */
+    private String buildRawJA4HString(String method, String path, String version, List<String> headers) {
+        List<String> parts = new ArrayList<>();
+        parts.add(method);
+        parts.add(path);
+        parts.add(version);
+        parts.addAll(headers);
+        // 用逗号分隔所有部分
+        return String.join(",", parts);
+    }
+
+    /**
+     * 计算字符串的MD5哈希
+     */
+    private String computeMD5(String input) throws NoSuchAlgorithmException {
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
+        
+        // 转换为16进制字符串
+        StringBuilder hexString = new StringBuilder();
+        for (byte b : digest) {
+            String hex = String.format("%02x", b);
+            hexString.append(hex);
+        }
+        return hexString.toString();
+    }
+
+    // 示例用法
+    public static void main(String[] args) {
+        // 模拟一个HTTP请求
+        String method = "GET";
+        String path = "/api/data?param=123"; // 路径会自动去除查询参数
+        String httpVersion = "HTTP/1.1";
+        
+        Map<String, String> headers = new HashMap<>();
+        headers.put("Accept", "text/html, application/xhtml+xml");
+        headers.put("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/114.0.0.0");
+        headers.put("Accept-Encoding", "gzip, deflate, br");
+        headers.put("Connection", "keep-alive");
+        
+        JA4HGeneratorUtil generator = new JA4HGeneratorUtil();
+        String ja4h = generator.generateJA4H(method, path, httpVersion, headers);
+        System.out.println("Generated JA4H fingerprint: " + ja4h);
+    }
+}

+ 101 - 9
pig-marketing/pig-marketing-biz/src/main/java/com/pig4cloud/pig/marketing/service/impl/MarketingDataServiceImpl.java

@@ -9,6 +9,9 @@ import com.pig4cloud.pig.marketing.api.dto.data.MarketingDataReportDTO;
 import com.pig4cloud.pig.marketing.api.dto.data.PageFirstLevelDataDTO;
 import com.pig4cloud.pig.marketing.api.dto.data.PageSecondLevelDataDTO;
 import com.pig4cloud.pig.marketing.api.entity.*;
+import com.pig4cloud.pig.marketing.api.util.FingerprintCombinerUtil;
+import com.pig4cloud.pig.marketing.api.util.JA4GeneratorUtil;
+import com.pig4cloud.pig.marketing.api.util.JA4HGeneratorUtil;
 import com.pig4cloud.pig.marketing.api.vo.config.GetMarketingGlobalConfigVO;
 import com.pig4cloud.pig.marketing.api.vo.data.MarketingDataReportVO;
 import com.pig4cloud.pig.marketing.api.vo.data.PageFirstLevelDataVO;
@@ -25,8 +28,6 @@ import org.springframework.stereotype.Service;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.*;
@@ -72,6 +73,8 @@ public class MarketingDataServiceImpl implements MarketingDataService {
 		// 获取全局配置
 		GetMarketingGlobalConfigVO config = marketingConfigService.getMarketingGlobalConfig();
 
+		String domain = getDomainFromRequest(request);
+
 		MarketingDataReportVO result = new MarketingDataReportVO();
 		BeanUtils.copyProperties(config,  result);
 		result.setType("link");
@@ -79,30 +82,120 @@ public class MarketingDataServiceImpl implements MarketingDataService {
 		// 获取客户端IP地址
 		String ip = IPUtils.getIpAddress(request);
 		// 获取域名
-		String domain = reqDto.getDomain();
+//		String domain = reqDto.getDomain();
 		// 应用匹配
 		MarketingApps marketingApps = matchedApp(ip, domain);
+
+		// 生成指纹
+		String fingerprint = FingerprintCombinerUtil.combine(reqDto.getFingerprint(), generateFingerprint(request));
+
 		// 未匹配到应用,返回默认信息
 		if (marketingApps == null){
+			result.setUrl(getTriggeredUrl(ip, fingerprint,config.getUrl(), config.getTriggerNum()));
 			return result;
 		}
 
+
 		// 入库
 		MarketingData marketingData = new MarketingData();
 		BeanUtils.copyProperties(reqDto, marketingData);
+		marketingData.setFingerprint(fingerprint);
 		marketingData.setIp( ip);
 		marketingData.setAppId(marketingApps.getAppId());
 		marketingDataMapper.insert(marketingData);
 
 		// 计算是否达到频率
 		result.setAppId(marketingApps.getAppId());
-		result.setUrl(getTriggeredUrl(marketingApps, ip));
+		result.setUrl(getTriggeredUrl(ip, fingerprint,marketingApps.getAppUrl(), marketingApps.getTriggerNum()));
 
 		// 返回结果
 
 		return result;
 	}
 
+	/**
+	 * 从HttpServletRequest中提取域名信息
+	 * @param request 请求对象
+	 * @return 域名(如example.com),异常时返回null
+	 */
+	private String getDomainFromRequest(HttpServletRequest request) {
+		try {
+			// 1. 优先从请求的服务器名称获取(包含端口时需处理)
+			String serverName = request.getServerName();
+			if (serverName != null && !serverName.trim().isEmpty()) {
+				return serverName.trim();
+			}
+
+			// 2. 若serverName为空,尝试从Host头获取(可能包含端口,如example.com:8080)
+			String hostHeader = request.getHeader("Host");
+			if (hostHeader != null && !hostHeader.trim().isEmpty()) {
+				// 去除端口部分(若有)
+				int colonIndex = hostHeader.indexOf(':');
+				return colonIndex != -1 ? hostHeader.substring(0, colonIndex).trim() : hostHeader.trim();
+			}
+
+			// 3. 若Host头也为空,尝试从请求URL中解析
+			String requestUrl = request.getRequestURL().toString();
+			if (requestUrl != null && !requestUrl.trim().isEmpty()) {
+				// 解析URL(如https://example.com/path → 提取example.com)
+				java.net.URL url = new java.net.URL(requestUrl);
+				return url.getHost();
+			}
+		} catch (Exception e) {
+			// 记录异常,但不影响主流程
+			log.error("获取域名信息失败", e);
+		}
+
+		// 所有方式失败时,返回默认值或null
+		return null;
+	}
+
+	/**
+	 * 根据请求协议类型生成对应指纹
+	 * @param request HTTP请求对象
+	 * @return 包含指纹和指纹类型的Map,key分别为"fingerprint"和"fingerprintType"
+	 */
+	private String generateFingerprint(HttpServletRequest request) {
+		String fingerprint = null;
+		String fingerprintType = null;
+		String scheme = request.getScheme().toLowerCase();
+
+		try {
+			if ("https".equals(scheme)) {
+				// HTTPS请求 - 生成JA4指纹
+				JA4GeneratorUtil ja4GeneratorUtil = new JA4GeneratorUtil();
+				fingerprint = ja4GeneratorUtil.generateJA4(request);
+				fingerprintType = "ja4";
+			} else if ("http".equals(scheme)) {
+				// HTTP请求 - 生成JA4H指纹
+				// 提取HTTP请求信息
+				String method = request.getMethod();
+				String path = request.getRequestURI();
+				String httpVersion = request.getProtocol();
+
+				// 提取请求头
+				Map<String, String> headers = new HashMap<>();
+				Enumeration<String> headerNames = request.getHeaderNames();
+				if (headerNames != null) {
+					while (headerNames.hasMoreElements()) {
+						String headerName = headerNames.nextElement();
+						headers.put(headerName, request.getHeader(headerName));
+					}
+				}
+
+				// 生成JA4H指纹
+				JA4HGeneratorUtil ja4HGeneratorUtil = new JA4HGeneratorUtil();
+				fingerprint = ja4HGeneratorUtil.generateJA4H(method, path, httpVersion, headers);
+				fingerprintType = "ja4h";
+			}
+		} catch (Exception e) {
+			// 指纹生成失败时记录日志,但不影响主流程
+			log.error("生成指纹失败:{}", e.getMessage());
+		}
+
+		return fingerprint;
+	}
+
 	/**
 	 * 分页统计一级营销数据
 	 * @param reqDto 分页参数
@@ -196,16 +289,15 @@ public class MarketingDataServiceImpl implements MarketingDataService {
 	/**
 	 * 根据triggerNum和Redis计数决定是否返回URL
 	 */
-	private String getTriggeredUrl(MarketingApps app, String userKey) {
+	private String getTriggeredUrl(String ip, String fingerprint, String url, String num) {
 
-		if (app == null || userKey == null) {
+		if (fingerprint == null || ip == null) {
 			return "";
 		}
 
-		BigDecimal triggerNum = new BigDecimal(app.getTriggerNum())
+		BigDecimal triggerNum = new BigDecimal(num)
 				.setScale(2, RoundingMode.HALF_UP);
-		String url = app.getAppUrl();
-		String countKey = String.format("marketing:trigger:count:%s:%s", app.getAppId(), userKey);
+		String countKey = String.format("marketing:trigger:count:%s:%s", fingerprint, ip);
 
 		// 1. 概率模式(triggerNum < 1)
 		if (triggerNum.compareTo(BigDecimal.ONE) < 0) {

+ 5 - 0
pig-marketing/pig-marketing-biz/src/main/resources/application.yml

@@ -22,4 +22,9 @@ spring:
       - nacos:application.yml
       - nacos:${spring.application.name}.yml
 
+marketing:
+  app:
+    url: http://192.168.3.17:2888/ipa/getApps
+    access-key: 4ea5ba93-d222-45dc-862a-1c7fc7789d11
+
 

+ 1 - 1
pig-marketing/pig-marketing-biz/src/main/resources/mapper/MarketingDataMapper.xml

@@ -59,7 +59,7 @@
 	<select id="countSecondLevelMarketingData"
 			resultType="com.pig4cloud.pig.marketing.api.vo.data.PageSecondLevelDataVO">
         SELECT
-            domain, referrer, fingerprint, COUNT (*) AS total, SUM (CASE WHEN create_time >= #{lastHourTime} THEN 1 ELSE 0 END) AS hourly, SUM (CASE WHEN create_time >=
+            domain, referrer, fingerprint, COUNT(*) AS total, SUM(CASE WHEN create_time >= #{lastHourTime} THEN 1 ELSE 0 END) AS hourly, SUM(CASE WHEN create_time >=
                                                                                              #{todayStartTime} THEN 1 ELSE 0 END) AS daily
         FROM
             marketing_data