|
@@ -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();
|
|
|
+ }
|
|
|
+}
|