附录A:完整代码清单

1 阅读16分钟

附录A:完整代码清单

声明:本文中的公司名称、包名、API地址、密钥等均已脱敏处理,使用虚构名称替代。本文仅用于安全研究和技术教学目的。

脱敏说明

  • 公司名称:梦想世界(DreamWorld)
  • 包名前缀:com.dreamworld.*
  • API域名:api.dreamworld.com
  • 请求头前缀:X-DW-*
  • SO库名称:libSecurityCore.so

目录

  1. 核心签名生成器
  2. Unidbg模拟器封装
  3. HTTP客户端封装
  4. 数据抓取服务
  5. 配置管理
  6. 工具类集合
  7. 测试用例

1. 核心签名生成器

1.1 SecurityChainGenerator.java

这是整个项目的核心类,负责调用Native层生成签名。

package com.dreamworld.security;

import com.github.unidbg.AndroidEmulator;
import com.github.unidbg.Module;
import com.github.unidbg.linux.android.AndroidEmulatorBuilder;
import com.github.unidbg.linux.android.AndroidResolver;
import com.github.unidbg.linux.android.dvm.*;
import com.github.unidbg.memory.Memory;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 安全链签名生成器
 * 
 * 核心功能:
 * 1. 初始化Unidbg模拟器环境
 * 2. 加载libSecurityCore.so
 * 3. 调用Native方法生成签名
 * 4. 管理模拟器生命周期
 * 
 * @author Security Research Team
 * @version 1.0.0
 */
public class SecurityChainGenerator extends AbstractJni implements Closeable {
    
    // ==================== 常量定义 ====================
    
    /** APK文件路径 */
    private static final String APK_PATH = "data/dreamworld-app.apk";
    
    /** SO库路径 */
    private static final String SO_PATH = "data/libSecurityCore.so";
    
    /** 目标SDK版本 */
    private static final int TARGET_SDK = 23;
    
    /** 进程名 */
    private static final String PROCESS_NAME = "com.dreamworld.app";
    
    // ==================== 实例变量 ====================
    
    private final AndroidEmulator emulator;
    private final VM vm;
    private final Module module;
    private final DvmClass securityClass;
    
    /** 缓存的签名结果 */
    private final Map<String, CachedSignature> signatureCache;
    
    /** 缓存过期时间(毫秒) */
    private static final long CACHE_TTL = 60000;
    
    // ==================== 构造函数 ====================
    
    /**
     * 创建签名生成器实例
     * 
     * @throws IOException 如果初始化失败
     */
    public SecurityChainGenerator() throws IOException {
        this(false);
    }
    
    /**
     * 创建签名生成器实例
     * 
     * @param enableDebug 是否启用调试模式
     * @throws IOException 如果初始化失败
     */
    public SecurityChainGenerator(boolean enableDebug) throws IOException {
        // 初始化缓存
        this.signatureCache = new ConcurrentHashMap<>();
        
        // 创建模拟器
        this.emulator = createEmulator(enableDebug);
        
        // 创建虚拟机
        this.vm = createVM();
        
        // 加载SO库
        this.module = loadNativeLibrary();
        
        // 获取安全类
        this.securityClass = vm.resolveClass("com/dreamworld/security/SecurityCore");
        
        // 调用JNI_OnLoad
        callJniOnLoad();
    }

    // ==================== 初始化方法 ====================
    
    /**
     * 创建Android模拟器
     */
    private AndroidEmulator createEmulator(boolean enableDebug) {
        AndroidEmulatorBuilder builder = AndroidEmulatorBuilder
            .for64Bit()
            .setProcessName(PROCESS_NAME);
        
        if (enableDebug) {
            builder.addBackendFactory(new DynarmicFactory(true));
        }
        
        AndroidEmulator emu = builder.build();
        
        // 配置内存
        Memory memory = emu.getMemory();
        memory.setLibraryResolver(new AndroidResolver(TARGET_SDK));
        
        return emu;
    }
    
    /**
     * 创建Dalvik虚拟机
     */
    private VM createVM() {
        VM dalvikVM = emulator.createDalvikVM(new File(APK_PATH));
        dalvikVM.setJni(this);
        dalvikVM.setVerbose(false);
        return dalvikVM;
    }
    
    /**
     * 加载Native库
     */
    private Module loadNativeLibrary() {
        DalvikModule dm = vm.loadLibrary(new File(SO_PATH), false);
        return dm.getModule();
    }
    
    /**
     * 调用JNI_OnLoad初始化
     */
    private void callJniOnLoad() {
        vm.callJNI_OnLoad(emulator, module);
    }
    
    // ==================== 核心签名方法 ====================
    
    /**
     * 生成安全链签名
     * 
     * @param url 请求URL
     * @param timestamp 时间戳
     * @param nonce 随机数
     * @param body 请求体(可为null)
     * @return 签名结果
     */
    public SignatureResult generateSignature(String url, long timestamp, 
                                             String nonce, String body) {
        // 检查缓存
        String cacheKey = buildCacheKey(url, timestamp, nonce, body);
        CachedSignature cached = signatureCache.get(cacheKey);
        if (cached != null && !cached.isExpired()) {
            return cached.getSignature();
        }
        
        // 生成新签名
        SignatureResult result = doGenerateSignature(url, timestamp, nonce, body);
        
        // 存入缓存
        signatureCache.put(cacheKey, new CachedSignature(result));
        
        return result;
    }
    
    /**
     * 实际执行签名生成
     */
    private SignatureResult doGenerateSignature(String url, long timestamp,
                                                String nonce, String body) {
        try {
            // 准备参数
            StringObject urlObj = new StringObject(vm, url);
            StringObject nonceObj = new StringObject(vm, nonce);
            StringObject bodyObj = body != null ? new StringObject(vm, body) : null;
            
            // 调用Native方法
            DvmObject<?> result = securityClass.callStaticJniMethodObject(
                emulator,
                "generateSecurityChain(Ljava/lang/String;JLjava/lang/String;" +
                "Ljava/lang/String;)Lcom/dreamworld/security/SecurityResult;",
                urlObj, timestamp, nonceObj, bodyObj
            );
            
            // 解析结果
            return parseResult(result);
            
        } catch (Exception e) {
            throw new SignatureGenerationException("签名生成失败", e);
        }
    }

    /**
     * 解析Native返回的结果对象
     */
    private SignatureResult parseResult(DvmObject<?> result) {
        if (result == null) {
            throw new SignatureGenerationException("Native返回null");
        }
        
        // 获取各个字段
        String signature = getStringField(result, "signature");
        String deviceId = getStringField(result, "deviceId");
        String sessionToken = getStringField(result, "sessionToken");
        long expireTime = getLongField(result, "expireTime");
        
        return new SignatureResult(signature, deviceId, sessionToken, expireTime);
    }
    
    /**
     * 获取对象的字符串字段
     */
    private String getStringField(DvmObject<?> obj, String fieldName) {
        DvmObject<?> field = obj.getObjectValue(fieldName);
        return field != null ? field.getValue().toString() : null;
    }
    
    /**
     * 获取对象的长整型字段
     */
    private long getLongField(DvmObject<?> obj, String fieldName) {
        Number value = obj.getIntValue(fieldName);
        return value != null ? value.longValue() : 0L;
    }
    
    // ==================== JNI回调处理 ====================
    
    @Override
    public DvmObject<?> callStaticObjectMethodV(BaseVM vm, DvmClass dvmClass,
                                                String signature, VaList vaList) {
        switch (signature) {
            case "android/os/Build->BRAND:Ljava/lang/String;":
                return new StringObject(vm, "DreamWorld");
                
            case "android/os/Build->MODEL:Ljava/lang/String;":
                return new StringObject(vm, "DW-X1");
                
            case "android/os/Build->DEVICE:Ljava/lang/String;":
                return new StringObject(vm, "dreamworld_x1");
                
            case "android/os/Build->FINGERPRINT:Ljava/lang/String;":
                return new StringObject(vm, 
                    "DreamWorld/DW-X1/dreamworld_x1:12/SKQ1.211006.001/" +
                    "V14.0.1.0.SKBCNXM:user/release-keys");
                
            case "android/provider/Settings$Secure->getString" +
                 "(Landroid/content/ContentResolver;Ljava/lang/String;)" +
                 "Ljava/lang/String;":
                String key = vaList.getObjectArg(1).getValue().toString();
                return handleSecureSettings(key);
                
            default:
                return super.callStaticObjectMethodV(vm, dvmClass, signature, vaList);
        }
    }
    
    /**
     * 处理Settings.Secure查询
     */
    private DvmObject<?> handleSecureSettings(String key) {
        switch (key) {
            case "android_id":
                return new StringObject(vm, generateAndroidId());
            default:
                return null;
        }
    }
    
    /**
     * 生成稳定的Android ID
     */
    private String generateAndroidId() {
        try {
            String seed = "dreamworld_device_" + System.getProperty("user.name");
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hash = md.digest(seed.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hash).substring(0, 16);
        } catch (Exception e) {
            return "a1b2c3d4e5f67890";
        }
    }

    @Override
    public int callStaticIntMethodV(BaseVM vm, DvmClass dvmClass, 
                                    String signature, VaList vaList) {
        switch (signature) {
            case "android/os/Build$VERSION->SDK_INT:I":
                return TARGET_SDK;
                
            case "android/os/Process->myPid()I":
                return 12345;
                
            case "android/os/Process->myUid()I":
                return 10086;
                
            default:
                return super.callStaticIntMethodV(vm, dvmClass, signature, vaList);
        }
    }
    
    @Override
    public DvmObject<?> callObjectMethodV(BaseVM vm, DvmObject<?> dvmObject,
                                          String signature, VaList vaList) {
        switch (signature) {
            case "android/content/Context->getPackageName()Ljava/lang/String;":
                return new StringObject(vm, PROCESS_NAME);
                
            case "android/content/Context->getFilesDir()Ljava/io/File;":
                return vm.resolveClass("java/io/File")
                    .newObject(new File("/data/data/" + PROCESS_NAME + "/files"));
                
            case "android/content/pm/PackageManager->getPackageInfo" +
                 "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;":
                return createPackageInfo();
                
            case "android/content/pm/Signature->toByteArray()[B":
                return createFakeSignature();
                
            default:
                return super.callObjectMethodV(vm, dvmObject, signature, vaList);
        }
    }
    
    /**
     * 创建PackageInfo对象
     */
    private DvmObject<?> createPackageInfo() {
        DvmClass packageInfoClass = vm.resolveClass("android/content/pm/PackageInfo");
        DvmObject<?> packageInfo = packageInfoClass.newObject(null);
        
        // 设置版本信息
        packageInfo.setIntValue("versionCode", 8170);
        packageInfo.setObjectValue("versionName", new StringObject(vm, "8.x.x"));
        
        // 设置签名数组
        DvmClass signatureClass = vm.resolveClass("android/content/pm/Signature");
        ArrayObject signatures = new ArrayObject(signatureClass.newObject(null));
        packageInfo.setObjectValue("signatures", signatures);
        
        return packageInfo;
    }
    
    /**
     * 创建伪造的APK签名
     */
    private DvmObject<?> createFakeSignature() {
        // 返回一个固定的签名字节数组
        byte[] fakeSignature = new byte[]{
            0x30, (byte)0x82, 0x02, (byte)0xA1, 0x30, (byte)0x82, 0x01,
            (byte)0x89, (byte)0xA0, 0x03, 0x02, 0x01, 0x02, 0x02, 0x04
            // ... 省略其余字节
        };
        return new ByteArray(vm, fakeSignature);
    }
    
    // ==================== 缓存管理 ====================
    
    /**
     * 构建缓存键
     */
    private String buildCacheKey(String url, long timestamp, String nonce, String body) {
        StringBuilder sb = new StringBuilder();
        sb.append(url).append("|");
        sb.append(timestamp).append("|");
        sb.append(nonce).append("|");
        sb.append(body != null ? body.hashCode() : "null");
        return sb.toString();
    }
    
    /**
     * 清理过期缓存
     */
    public void cleanExpiredCache() {
        signatureCache.entrySet().removeIf(entry -> entry.getValue().isExpired());
    }
    
    /**
     * 清空所有缓存
     */
    public void clearCache() {
        signatureCache.clear();
    }

    // ==================== 工具方法 ====================
    
    /**
     * 字节数组转十六进制字符串
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    
    // ==================== 资源管理 ====================
    
    @Override
    public void close() {
        if (emulator != null) {
            emulator.close();
        }
        signatureCache.clear();
    }
    
    // ==================== 内部类 ====================
    
    /**
     * 缓存的签名
     */
    private static class CachedSignature {
        private final SignatureResult signature;
        private final long createTime;
        
        public CachedSignature(SignatureResult signature) {
            this.signature = signature;
            this.createTime = System.currentTimeMillis();
        }
        
        public boolean isExpired() {
            return System.currentTimeMillis() - createTime > CACHE_TTL;
        }
        
        public SignatureResult getSignature() {
            return signature;
        }
    }
}

1.2 SignatureResult.java

签名结果数据类。

package com.dreamworld.security;

import java.io.Serializable;

/**
 * 签名结果
 */
public class SignatureResult implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    private final String signature;
    private final String deviceId;
    private final String sessionToken;
    private final long expireTime;
    private final long createTime;
    
    public SignatureResult(String signature, String deviceId, 
                          String sessionToken, long expireTime) {
        this.signature = signature;
        this.deviceId = deviceId;
        this.sessionToken = sessionToken;
        this.expireTime = expireTime;
        this.createTime = System.currentTimeMillis();
    }
    
    public String getSignature() {
        return signature;
    }
    
    public String getDeviceId() {
        return deviceId;
    }
    
    public String getSessionToken() {
        return sessionToken;
    }
    
    public long getExpireTime() {
        return expireTime;
    }
    
    public boolean isExpired() {
        return System.currentTimeMillis() > expireTime;
    }
    
    public long getAge() {
        return System.currentTimeMillis() - createTime;
    }
    
    @Override
    public String toString() {
        return String.format(
            "SignatureResult{signature='%s...', deviceId='%s', expired=%s}",
            signature.substring(0, Math.min(16, signature.length())),
            deviceId,
            isExpired()
        );
    }
}

2. Unidbg模拟器封装

2.1 EmulatorPool.java

模拟器对象池,用于高并发场景。

package com.dreamworld.security.pool;

import com.dreamworld.security.SecurityChainGenerator;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.time.Duration;

/**
 * 模拟器对象池
 * 
 * 使用Apache Commons Pool2实现对象池化,
 * 避免频繁创建/销毁模拟器实例带来的性能开销。
 */
public class EmulatorPool implements AutoCloseable {
    
    private final GenericObjectPool<SecurityChainGenerator> pool;
    
    /**
     * 创建默认配置的对象池
     */
    public EmulatorPool() {
        this(createDefaultConfig());
    }
    
    /**
     * 创建自定义配置的对象池
     */
    public EmulatorPool(GenericObjectPoolConfig<SecurityChainGenerator> config) {
        this.pool = new GenericObjectPool<>(new EmulatorFactory(), config);
    }
    
    /**
     * 创建默认配置
     */
    private static GenericObjectPoolConfig<SecurityChainGenerator> createDefaultConfig() {
        GenericObjectPoolConfig<SecurityChainGenerator> config = 
            new GenericObjectPoolConfig<>();
        
        // 池大小配置
        config.setMinIdle(2);
        config.setMaxIdle(8);
        config.setMaxTotal(16);
        
        // 等待配置
        config.setMaxWait(Duration.ofSeconds(30));
        config.setBlockWhenExhausted(true);
        
        // 驱逐配置
        config.setTimeBetweenEvictionRuns(Duration.ofMinutes(5));
        config.setMinEvictableIdleTime(Duration.ofMinutes(30));
        
        // 验证配置
        config.setTestOnBorrow(true);
        config.setTestOnReturn(false);
        config.setTestWhileIdle(true);
        
        return config;
    }
    
    /**
     * 借用模拟器实例
     */
    public SecurityChainGenerator borrow() throws Exception {
        return pool.borrowObject();
    }
    
    /**
     * 归还模拟器实例
     */
    public void returnObject(SecurityChainGenerator generator) {
        if (generator != null) {
            pool.returnObject(generator);
        }
    }
    
    /**
     * 使模拟器实例失效
     */
    public void invalidate(SecurityChainGenerator generator) throws Exception {
        if (generator != null) {
            pool.invalidateObject(generator);
        }
    }
    
    /**
     * 获取池状态
     */
    public PoolStats getStats() {
        return new PoolStats(
            pool.getNumActive(),
            pool.getNumIdle(),
            pool.getNumWaiters(),
            pool.getBorrowedCount(),
            pool.getReturnedCount(),
            pool.getCreatedCount(),
            pool.getDestroyedCount()
        );
    }
    
    @Override
    public void close() {
        pool.close();
    }
    
    /**
     * 模拟器工厂
     */
    private static class EmulatorFactory 
            extends BasePooledObjectFactory<SecurityChainGenerator> {
        
        @Override
        public SecurityChainGenerator create() throws Exception {
            return new SecurityChainGenerator();
        }
        
        @Override
        public PooledObject<SecurityChainGenerator> wrap(SecurityChainGenerator obj) {
            return new DefaultPooledObject<>(obj);
        }
        
        @Override
        public void destroyObject(PooledObject<SecurityChainGenerator> p) {
            p.getObject().close();
        }
        
        @Override
        public boolean validateObject(PooledObject<SecurityChainGenerator> p) {
            try {
                // 简单验证:尝试生成一个签名
                SecurityChainGenerator gen = p.getObject();
                gen.generateSignature("/test", System.currentTimeMillis(), 
                                      "test", null);
                return true;
            } catch (Exception e) {
                return false;
            }
        }
        
        @Override
        public void passivateObject(PooledObject<SecurityChainGenerator> p) {
            // 归还时清理缓存
            p.getObject().cleanExpiredCache();
        }
    }
    
    /**
     * 池状态统计
     */
    public static class PoolStats {
        public final int active;
        public final int idle;
        public final int waiters;
        public final long borrowed;
        public final long returned;
        public final long created;
        public final long destroyed;
        
        public PoolStats(int active, int idle, int waiters, long borrowed,
                        long returned, long created, long destroyed) {
            this.active = active;
            this.idle = idle;
            this.waiters = waiters;
            this.borrowed = borrowed;
            this.returned = returned;
            this.created = created;
            this.destroyed = destroyed;
        }
        
        @Override
        public String toString() {
            return String.format(
                "PoolStats{active=%d, idle=%d, waiters=%d, " +
                "borrowed=%d, returned=%d, created=%d, destroyed=%d}",
                active, idle, waiters, borrowed, returned, created, destroyed
            );
        }
    }
}

2.2 SignatureService.java

签名服务,封装对象池的使用。

package com.dreamworld.security.service;

import com.dreamworld.security.SecurityChainGenerator;
import com.dreamworld.security.SignatureResult;
import com.dreamworld.security.pool.EmulatorPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 签名服务
 * 
 * 提供线程安全的签名生成服务,
 * 内部使用对象池管理模拟器实例。
 */
@Service
public class SignatureService {
    
    private static final Logger logger = LoggerFactory.getLogger(SignatureService.class);
    
    private EmulatorPool pool;
    
    private final AtomicLong requestCount = new AtomicLong(0);
    private final AtomicLong successCount = new AtomicLong(0);
    private final AtomicLong failureCount = new AtomicLong(0);
    private final AtomicLong totalTime = new AtomicLong(0);
    
    @PostConstruct
    public void init() {
        logger.info("初始化签名服务...");
        this.pool = new EmulatorPool();
        logger.info("签名服务初始化完成");
    }
    
    @PreDestroy
    public void destroy() {
        logger.info("关闭签名服务...");
        if (pool != null) {
            pool.close();
        }
        logger.info("签名服务已关闭");
    }
    
    /**
     * 生成签名
     * 
     * @param url 请求URL
     * @param body 请求体(可为null)
     * @return 签名结果
     */
    public SignatureResult generateSignature(String url, String body) {
        long startTime = System.currentTimeMillis();
        requestCount.incrementAndGet();
        
        SecurityChainGenerator generator = null;
        boolean success = false;
        
        try {
            // 从池中借用
            generator = pool.borrow();
            
            // 生成签名参数
            long timestamp = System.currentTimeMillis();
            String nonce = generateNonce();
            
            // 调用签名生成
            SignatureResult result = generator.generateSignature(
                url, timestamp, nonce, body
            );
            
            success = true;
            successCount.incrementAndGet();
            
            return result;
            
        } catch (Exception e) {
            failureCount.incrementAndGet();
            logger.error("签名生成失败: url={}", url, e);
            
            // 使实例失效
            if (generator != null) {
                try {
                    pool.invalidate(generator);
                    generator = null;
                } catch (Exception ex) {
                    logger.warn("使实例失效时出错", ex);
                }
            }
            
            throw new SignatureServiceException("签名生成失败", e);
            
        } finally {
            // 归还实例
            if (generator != null) {
                pool.returnObject(generator);
            }
            
            // 记录耗时
            long elapsed = System.currentTimeMillis() - startTime;
            totalTime.addAndGet(elapsed);
            
            if (elapsed > 1000) {
                logger.warn("签名生成耗时过长: {}ms, url={}", elapsed, url);
            }
        }
    }
    
    /**
     * 生成随机数
     */
    private String generateNonce() {
        return UUID.randomUUID().toString().replace("-", "");
    }
    
    /**
     * 获取服务统计信息
     */
    public ServiceStats getStats() {
        long requests = requestCount.get();
        long successes = successCount.get();
        long failures = failureCount.get();
        long time = totalTime.get();
        
        return new ServiceStats(
            requests,
            successes,
            failures,
            requests > 0 ? (double) time / requests : 0,
            requests > 0 ? (double) successes / requests * 100 : 0,
            pool.getStats()
        );
    }
    
    /**
     * 服务统计
     */
    public static class ServiceStats {
        public final long totalRequests;
        public final long successRequests;
        public final long failedRequests;
        public final double avgResponseTime;
        public final double successRate;
        public final EmulatorPool.PoolStats poolStats;
        
        public ServiceStats(long totalRequests, long successRequests,
                           long failedRequests, double avgResponseTime,
                           double successRate, EmulatorPool.PoolStats poolStats) {
            this.totalRequests = totalRequests;
            this.successRequests = successRequests;
            this.failedRequests = failedRequests;
            this.avgResponseTime = avgResponseTime;
            this.successRate = successRate;
            this.poolStats = poolStats;
        }
    }
}

3. HTTP客户端封装

3.1 DreamWorldApiClient.java

封装了签名逻辑的API客户端。

package com.dreamworld.client;

import com.dreamworld.security.SignatureResult;
import com.dreamworld.security.service.SignatureService;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 梦想世界API客户端
 * 
 * 自动处理签名、重试、错误处理等逻辑。
 */
public class DreamWorldApiClient {
    
    private static final Logger logger = LoggerFactory.getLogger(DreamWorldApiClient.class);
    
    private static final String BASE_URL = "https://api.dreamworld.com";
    private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
    
    private final OkHttpClient httpClient;
    private final SignatureService signatureService;
    private final ObjectMapper objectMapper;
    
    // 设备信息
    private final String deviceId;
    private final String appVersion;
    private final String osVersion;
    
    public DreamWorldApiClient(SignatureService signatureService) {
        this.signatureService = signatureService;
        this.objectMapper = new ObjectMapper();
        
        // 配置HTTP客户端
        this.httpClient = new OkHttpClient.Builder()
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .addInterceptor(new LoggingInterceptor())
            .addInterceptor(new RetryInterceptor(3))
            .build();
        
        // 设备信息
        this.deviceId = "DW" + System.currentTimeMillis();
        this.appVersion = "8.x.x";
        this.osVersion = "Android 12";
    }
    
    /**
     * 发送GET请求
     */
    public <T> T get(String path, Class<T> responseType) throws IOException {
        return get(path, null, responseType);
    }
    
    /**
     * 发送带参数的GET请求
     */
    public <T> T get(String path, Map<String, String> params, 
                     Class<T> responseType) throws IOException {
        // 构建URL
        HttpUrl.Builder urlBuilder = HttpUrl.parse(BASE_URL + path).newBuilder();
        if (params != null) {
            params.forEach(urlBuilder::addQueryParameter);
        }
        String url = urlBuilder.build().toString();
        
        // 生成签名
        SignatureResult signature = signatureService.generateSignature(url, null);
        
        // 构建请求
        Request request = new Request.Builder()
            .url(url)
            .headers(buildHeaders(signature))
            .get()
            .build();
        
        // 执行请求
        return executeRequest(request, responseType);
    }
    
    /**
     * 发送POST请求
     */
    public <T> T post(String path, Object body, Class<T> responseType) throws IOException {
        String url = BASE_URL + path;
        String bodyJson = objectMapper.writeValueAsString(body);
        
        // 生成签名
        SignatureResult signature = signatureService.generateSignature(url, bodyJson);
        
        // 构建请求
        Request request = new Request.Builder()
            .url(url)
            .headers(buildHeaders(signature))
            .post(RequestBody.create(bodyJson, JSON))
            .build();
        
        // 执行请求
        return executeRequest(request, responseType);
    }
    
    /**
     * 构建请求头
     */
    private Headers buildHeaders(SignatureResult signature) {
        return new Headers.Builder()
            // 签名相关
            .add("X-DW-Signature", signature.getSignature())
            .add("X-DW-DeviceId", signature.getDeviceId())
            .add("X-DW-SessionToken", signature.getSessionToken())
            .add("X-DW-Timestamp", String.valueOf(System.currentTimeMillis()))
            .add("X-DW-Nonce", generateNonce())
            // 设备信息
            .add("X-DW-AppVersion", appVersion)
            .add("X-DW-OSVersion", osVersion)
            .add("X-DW-Platform", "Android")
            // 通用头
            .add("Content-Type", "application/json")
            .add("Accept", "application/json")
            .add("User-Agent", "DreamWorld/" + appVersion + " Android")
            .build();
    }
    
    /**
     * 执行请求
     */
    private <T> T executeRequest(Request request, Class<T> responseType) 
            throws IOException {
        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                handleErrorResponse(response);
            }
            
            String body = response.body().string();
            return objectMapper.readValue(body, responseType);
        }
    }
    
    /**
     * 处理错误响应
     */
    private void handleErrorResponse(Response response) throws IOException {
        int code = response.code();
        String body = response.body() != null ? response.body().string() : "";
        
        switch (code) {
            case 401:
                throw new AuthenticationException("认证失败: " + body);
            case 403:
                throw new ForbiddenException("访问被拒绝: " + body);
            case 429:
                throw new RateLimitException("请求过于频繁: " + body);
            case 500:
            case 502:
            case 503:
                throw new ServerException("服务器错误: " + code);
            default:
                throw new ApiException("API错误: " + code + " - " + body);
        }
    }
    
    private String generateNonce() {
        return java.util.UUID.randomUUID().toString().replace("-", "");
    }
}

3.2 RetryInterceptor.java

自动重试拦截器。

package com.dreamworld.client.interceptor;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Set;

/**
 * 重试拦截器
 * 
 * 对于可重试的错误自动进行重试。
 */
public class RetryInterceptor implements Interceptor {
    
    private static final Logger logger = LoggerFactory.getLogger(RetryInterceptor.class);
    
    /** 可重试的HTTP状态码 */
    private static final Set<Integer> RETRYABLE_CODES = Set.of(408, 429, 500, 502, 503, 504);
    
    private final int maxRetries;
    private final long baseDelayMs;
    
    public RetryInterceptor(int maxRetries) {
        this(maxRetries, 1000);
    }
    
    public RetryInterceptor(int maxRetries, long baseDelayMs) {
        this.maxRetries = maxRetries;
        this.baseDelayMs = baseDelayMs;
    }
    
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = null;
        IOException lastException = null;
        
        for (int attempt = 0; attempt <= maxRetries; attempt++) {
            try {
                // 关闭之前的响应
                if (response != null) {
                    response.close();
                }
                
                // 执行请求
                response = chain.proceed(request);
                
                // 检查是否需要重试
                if (response.isSuccessful() || !shouldRetry(response.code())) {
                    return response;
                }
                
                logger.warn("请求失败,准备重试: attempt={}, code={}, url={}",
                    attempt + 1, response.code(), request.url());
                
            } catch (IOException e) {
                lastException = e;
                logger.warn("请求异常,准备重试: attempt={}, error={}, url={}",
                    attempt + 1, e.getMessage(), request.url());
            }
            
            // 最后一次尝试不需要等待
            if (attempt < maxRetries) {
                sleep(calculateDelay(attempt));
            }
        }
        
        // 所有重试都失败
        if (lastException != null) {
            throw lastException;
        }
        
        return response;
    }
    
    /**
     * 判断是否应该重试
     */
    private boolean shouldRetry(int code) {
        return RETRYABLE_CODES.contains(code);
    }
    
    /**
     * 计算重试延迟(指数退避)
     */
    private long calculateDelay(int attempt) {
        // 指数退避 + 随机抖动
        long delay = baseDelayMs * (1L << attempt);
        long jitter = (long) (delay * 0.2 * Math.random());
        return Math.min(delay + jitter, 30000); // 最大30秒
    }
    
    private void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

4. 数据抓取服务

4.1 ProductCrawlerService.java

商品数据抓取服务。

package com.dreamworld.crawler;

import com.dreamworld.client.DreamWorldApiClient;
import com.dreamworld.crawler.model.*;
import com.dreamworld.crawler.storage.DataStorage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 商品抓取服务
 * 
 * 负责定时抓取商品数据并存储。
 */
@Service
public class ProductCrawlerService {
    
    private static final Logger logger = LoggerFactory.getLogger(ProductCrawlerService.class);
    
    private final DreamWorldApiClient apiClient;
    private final DataStorage storage;
    private final ExecutorService executor;
    
    // 抓取配置
    private static final int PAGE_SIZE = 20;
    private static final int MAX_PAGES = 100;
    private static final int CONCURRENT_REQUESTS = 5;
    private static final long REQUEST_INTERVAL_MS = 500;
    
    // 统计信息
    private final AtomicInteger totalCrawled = new AtomicInteger(0);
    private final AtomicInteger totalFailed = new AtomicInteger(0);
    
    public ProductCrawlerService(DreamWorldApiClient apiClient, DataStorage storage) {
        this.apiClient = apiClient;
        this.storage = storage;
        this.executor = Executors.newFixedThreadPool(CONCURRENT_REQUESTS);
    }
    
    /**
     * 定时抓取任务 - 每小时执行
     */
    @Scheduled(cron = "0 0 * * * *")
    public void scheduledCrawl() {
        logger.info("开始定时抓取任务...");
        try {
            crawlAllProducts();
        } catch (Exception e) {
            logger.error("定时抓取任务失败", e);
        }
    }
    
    /**
     * 抓取所有商品
     */
    public CrawlResult crawlAllProducts() {
        long startTime = System.currentTimeMillis();
        List<Product> allProducts = new ArrayList<>();
        int failedPages = 0;
        
        try {
            // 获取分类列表
            List<Category> categories = fetchCategories();
            logger.info("获取到 {} 个分类", categories.size());
            
            // 并发抓取各分类
            List<Future<List<Product>>> futures = new ArrayList<>();
            for (Category category : categories) {
                futures.add(executor.submit(() -> crawlCategory(category)));
            }
            
            // 收集结果
            for (Future<List<Product>> future : futures) {
                try {
                    allProducts.addAll(future.get(5, TimeUnit.MINUTES));
                } catch (Exception e) {
                    failedPages++;
                    logger.error("抓取分类失败", e);
                }
            }
            
            // 存储数据
            storage.saveProducts(allProducts);
            
            long elapsed = System.currentTimeMillis() - startTime;
            logger.info("抓取完成: 商品数={}, 耗时={}ms", allProducts.size(), elapsed);
            
            return new CrawlResult(allProducts.size(), failedPages, elapsed);
            
        } catch (Exception e) {
            logger.error("抓取失败", e);
            throw new CrawlerException("抓取失败", e);
        }
    }

    /**
     * 抓取单个分类的商品
     */
    private List<Product> crawlCategory(Category category) {
        List<Product> products = new ArrayList<>();
        int page = 1;
        
        while (page <= MAX_PAGES) {
            try {
                // 请求间隔
                Thread.sleep(REQUEST_INTERVAL_MS);
                
                // 获取商品列表
                ProductListResponse response = apiClient.get(
                    "/api/v1/products",
                    Map.of(
                        "categoryId", category.getId(),
                        "page", String.valueOf(page),
                        "pageSize", String.valueOf(PAGE_SIZE)
                    ),
                    ProductListResponse.class
                );
                
                if (response.getProducts().isEmpty()) {
                    break;
                }
                
                products.addAll(response.getProducts());
                totalCrawled.addAndGet(response.getProducts().size());
                
                logger.debug("抓取分类 {} 第 {} 页,获取 {} 条",
                    category.getName(), page, response.getProducts().size());
                
                // 检查是否还有更多
                if (!response.hasMore()) {
                    break;
                }
                
                page++;
                
            } catch (Exception e) {
                totalFailed.incrementAndGet();
                logger.error("抓取分类 {} 第 {} 页失败", category.getName(), page, e);
                break;
            }
        }
        
        return products;
    }
    
    /**
     * 获取分类列表
     */
    private List<Category> fetchCategories() throws Exception {
        CategoryListResponse response = apiClient.get(
            "/api/v1/categories",
            CategoryListResponse.class
        );
        return response.getCategories();
    }
    
    /**
     * 获取统计信息
     */
    public CrawlerStats getStats() {
        return new CrawlerStats(
            totalCrawled.get(),
            totalFailed.get(),
            storage.getProductCount()
        );
    }
    
    /**
     * 抓取结果
     */
    public static class CrawlResult {
        public final int productCount;
        public final int failedPages;
        public final long elapsedMs;
        
        public CrawlResult(int productCount, int failedPages, long elapsedMs) {
            this.productCount = productCount;
            this.failedPages = failedPages;
            this.elapsedMs = elapsedMs;
        }
    }
    
    /**
     * 抓取统计
     */
    public static class CrawlerStats {
        public final int totalCrawled;
        public final int totalFailed;
        public final int storedProducts;
        
        public CrawlerStats(int totalCrawled, int totalFailed, int storedProducts) {
            this.totalCrawled = totalCrawled;
            this.totalFailed = totalFailed;
            this.storedProducts = storedProducts;
        }
    }
}

5. 配置管理

5.1 application.yml

Spring Boot配置文件。

# 应用配置
spring:
  application:
    name: dreamworld-crawler
  
  # 数据源配置
  datasource:
    url: jdbc:mysql://localhost:3306/dreamworld?useSSL=false&serverTimezone=UTC
    username: ${DB_USERNAME:root}
    password: ${DB_PASSWORD:password}
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      minimum-idle: 5
      maximum-pool-size: 20
      idle-timeout: 300000
      max-lifetime: 1200000
      connection-timeout: 30000
  
  # Redis配置
  redis:
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    password: ${REDIS_PASSWORD:}
    database: 0
    lettuce:
      pool:
        min-idle: 2
        max-idle: 8
        max-active: 16

# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /api

# 签名服务配置
signature:
  pool:
    min-idle: 2
    max-idle: 8
    max-total: 16
    max-wait-seconds: 30
  cache:
    ttl-seconds: 60
    max-size: 10000

# 抓取配置
crawler:
  enabled: true
  page-size: 20
  max-pages: 100
  concurrent-requests: 5
  request-interval-ms: 500
  schedule:
    enabled: true
    cron: "0 0 * * * *"

# 监控配置
management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus
  endpoint:
    health:
      show-details: always
  metrics:
    export:
      prometheus:
        enabled: true

# 日志配置
logging:
  level:
    root: INFO
    com.dreamworld: DEBUG
    com.github.unidbg: WARN
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: logs/dreamworld-crawler.log
    max-size: 100MB
    max-history: 30

5.2 pom.xml

Maven项目配置。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    
    <groupId>com.dreamworld</groupId>
    <artifactId>dreamworld-crawler</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    
    <name>DreamWorld Crawler</name>
    <description>梦想世界数据抓取服务</description>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>
    
    <properties>
        <java.version>11</java.version>
        <unidbg.version>0.9.7</unidbg.version>
        <okhttp.version>4.12.0</okhttp.version>
    </properties>
    
    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        
        <!-- Unidbg -->
        <dependency>
            <groupId>com.github.zhkl0228</groupId>
            <artifactId>unidbg-android</artifactId>
            <version>${unidbg.version}</version>
        </dependency>
        
        <!-- HTTP Client -->
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>${okhttp.version}</version>
        </dependency>
        
        <!-- Object Pool -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.12.0</version>
        </dependency>
        
        <!-- Database -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        
        <!-- Monitoring -->
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
        </dependency>
        
        <!-- Utils -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>32.1.3-jre</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        
        <!-- Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
    <repositories>
        <repository>
            <id>jitpack.io</id>
            <url>https://jitpack.io</url>
        </repository>
    </repositories>
</project>

6. 工具类集合

6.1 CryptoUtils.java

加密工具类。

package com.dreamworld.util;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;

/**
 * 加密工具类
 */
public final class CryptoUtils {
    
    private CryptoUtils() {}
    
    /**
     * MD5哈希
     */
    public static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hash);
        } catch (Exception e) {
            throw new RuntimeException("MD5计算失败", e);
        }
    }
    
    /**
     * SHA256哈希
     */
    public static String sha256(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] hash = md.digest(input.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hash);
        } catch (Exception e) {
            throw new RuntimeException("SHA256计算失败", e);
        }
    }
    
    /**
     * HMAC-SHA256
     */
    public static String hmacSha256(String data, String key) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(
                key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKey);
            byte[] hash = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hash);
        } catch (Exception e) {
            throw new RuntimeException("HMAC-SHA256计算失败", e);
        }
    }
    
    /**
     * AES加密
     */
    public static String aesEncrypt(String data, String key, String iv) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(
                key.getBytes(StandardCharsets.UTF_8), "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(
                iv.getBytes(StandardCharsets.UTF_8));
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
            byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("AES加密失败", e);
        }
    }
    
    /**
     * AES解密
     */
    public static String aesDecrypt(String encryptedData, String key, String iv) {
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec keySpec = new SecretKeySpec(
                key.getBytes(StandardCharsets.UTF_8), "AES");
            IvParameterSpec ivSpec = new IvParameterSpec(
                iv.getBytes(StandardCharsets.UTF_8));
            cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("AES解密失败", e);
        }
    }
    
    /**
     * 字节数组转十六进制
     */
    public static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    
    /**
     * 十六进制转字节数组
     */
    public static byte[] hexToBytes(String hex) {
        int len = hex.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
                + Character.digit(hex.charAt(i + 1), 16));
        }
        return data;
    }
}

6.2 DeviceUtils.java

设备信息工具类。

package com.dreamworld.util;

import java.net.NetworkInterface;
import java.security.MessageDigest;
import java.util.Enumeration;
import java.util.UUID;

/**
 * 设备信息工具类
 */
public final class DeviceUtils {
    
    private DeviceUtils() {}
    
    private static String cachedDeviceId;
    
    /**
     * 获取设备ID
     */
    public static synchronized String getDeviceId() {
        if (cachedDeviceId == null) {
            cachedDeviceId = generateDeviceId();
        }
        return cachedDeviceId;
    }
    
    /**
     * 生成设备ID
     */
    private static String generateDeviceId() {
        try {
            // 尝试使用MAC地址
            String macAddress = getMacAddress();
            if (macAddress != null) {
                return md5(macAddress).substring(0, 16);
            }
            
            // 回退到随机UUID
            return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
            
        } catch (Exception e) {
            return UUID.randomUUID().toString().replace("-", "").substring(0, 16);
        }
    }
    
    /**
     * 获取MAC地址
     */
    private static String getMacAddress() {
        try {
            Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
            while (interfaces.hasMoreElements()) {
                NetworkInterface ni = interfaces.nextElement();
                byte[] mac = ni.getHardwareAddress();
                if (mac != null && mac.length > 0) {
                    StringBuilder sb = new StringBuilder();
                    for (byte b : mac) {
                        sb.append(String.format("%02X", b));
                    }
                    return sb.toString();
                }
            }
        } catch (Exception e) {
            // 忽略
        }
        return null;
    }
    
    /**
     * 生成随机IMEI
     */
    public static String generateImei() {
        StringBuilder sb = new StringBuilder();
        sb.append("86"); // 中国TAC
        for (int i = 0; i < 12; i++) {
            sb.append((int) (Math.random() * 10));
        }
        // 计算校验位
        sb.append(calculateLuhnCheckDigit(sb.toString()));
        return sb.toString();
    }
    
    /**
     * Luhn校验位计算
     */
    private static int calculateLuhnCheckDigit(String number) {
        int sum = 0;
        boolean alternate = true;
        for (int i = number.length() - 1; i >= 0; i--) {
            int n = Character.getNumericValue(number.charAt(i));
            if (alternate) {
                n *= 2;
                if (n > 9) {
                    n = (n % 10) + 1;
                }
            }
            sum += n;
            alternate = !alternate;
        }
        return (10 - (sum % 10)) % 10;
    }
    
    private static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hash = md.digest(input.getBytes());
            StringBuilder sb = new StringBuilder();
            for (byte b : hash) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

7. 测试用例

7.1 SecurityChainGeneratorTest.java

核心签名生成器测试。

package com.dreamworld.security;

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

/**
 * 签名生成器测试
 */
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SecurityChainGeneratorTest {
    
    private static SecurityChainGenerator generator;
    
    @BeforeAll
    static void setUp() throws Exception {
        generator = new SecurityChainGenerator();
    }
    
    @AfterAll
    static void tearDown() {
        if (generator != null) {
            generator.close();
        }
    }
    
    @Test
    @Order(1)
    @DisplayName("测试基本签名生成")
    void testBasicSignature() {
        // Given
        String url = "/api/v1/products";
        long timestamp = System.currentTimeMillis();
        String nonce = "abc123";
        
        // When
        SignatureResult result = generator.generateSignature(url, timestamp, nonce, null);
        
        // Then
        assertNotNull(result);
        assertNotNull(result.getSignature());
        assertFalse(result.getSignature().isEmpty());
        assertNotNull(result.getDeviceId());
    }
    
    @Test
    @Order(2)
    @DisplayName("测试带请求体的签名")
    void testSignatureWithBody() {
        // Given
        String url = "/api/v1/orders";
        long timestamp = System.currentTimeMillis();
        String nonce = "def456";
        String body = "{"productId":"12345","quantity":1}";
        
        // When
        SignatureResult result = generator.generateSignature(url, timestamp, nonce, body);
        
        // Then
        assertNotNull(result);
        assertNotNull(result.getSignature());
    }
    
    @Test
    @Order(3)
    @DisplayName("测试签名缓存")
    void testSignatureCache() {
        // Given
        String url = "/api/v1/test";
        long timestamp = System.currentTimeMillis();
        String nonce = "cache_test";
        
        // When - 第一次调用
        long start1 = System.nanoTime();
        SignatureResult result1 = generator.generateSignature(url, timestamp, nonce, null);
        long time1 = System.nanoTime() - start1;
        
        // When - 第二次调用(应该命中缓存)
        long start2 = System.nanoTime();
        SignatureResult result2 = generator.generateSignature(url, timestamp, nonce, null);
        long time2 = System.nanoTime() - start2;
        
        // Then
        assertEquals(result1.getSignature(), result2.getSignature());
        assertTrue(time2 < time1 / 10, "缓存命中应该快10倍以上");
    }
    
    @Test
    @Order(4)
    @DisplayName("测试不同参数生成不同签名")
    void testDifferentParams() {
        // Given
        long timestamp = System.currentTimeMillis();
        
        // When
        SignatureResult result1 = generator.generateSignature(
            "/api/v1/a", timestamp, "nonce1", null);
        SignatureResult result2 = generator.generateSignature(
            "/api/v1/b", timestamp, "nonce2", null);
        
        // Then
        assertNotEquals(result1.getSignature(), result2.getSignature());
    }
    
    @Test
    @Order(5)
    @DisplayName("测试并发签名生成")
    void testConcurrentSignature() throws Exception {
        int threadCount = 10;
        java.util.concurrent.CountDownLatch latch = 
            new java.util.concurrent.CountDownLatch(threadCount);
        java.util.concurrent.atomic.AtomicInteger successCount = 
            new java.util.concurrent.atomic.AtomicInteger(0);
        
        for (int i = 0; i < threadCount; i++) {
            final int index = i;
            new Thread(() -> {
                try {
                    SignatureResult result = generator.generateSignature(
                        "/api/v1/concurrent/" + index,
                        System.currentTimeMillis(),
                        "nonce_" + index,
                        null
                    );
                    if (result != null && result.getSignature() != null) {
                        successCount.incrementAndGet();
                    }
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        
        latch.await();
        assertEquals(threadCount, successCount.get(), "所有并发请求都应该成功");
    }
}

7.2 EmulatorPoolTest.java

对象池测试。

package com.dreamworld.security.pool;

import com.dreamworld.security.SecurityChainGenerator;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

/**
 * 对象池测试
 */
class EmulatorPoolTest {
    
    private EmulatorPool pool;
    
    @BeforeEach
    void setUp() {
        pool = new EmulatorPool();
    }
    
    @AfterEach
    void tearDown() {
        if (pool != null) {
            pool.close();
        }
    }
    
    @Test
    @DisplayName("测试借用和归还")
    void testBorrowAndReturn() throws Exception {
        // When
        SecurityChainGenerator gen = pool.borrow();
        
        // Then
        assertNotNull(gen);
        
        // 归还
        pool.returnObject(gen);
        
        // 验证池状态
        EmulatorPool.PoolStats stats = pool.getStats();
        assertEquals(0, stats.active);
        assertTrue(stats.idle > 0);
    }
    
    @Test
    @DisplayName("测试对象复用")
    void testObjectReuse() throws Exception {
        // 借用并归还
        SecurityChainGenerator gen1 = pool.borrow();
        pool.returnObject(gen1);
        
        // 再次借用
        SecurityChainGenerator gen2 = pool.borrow();
        
        // 应该是同一个对象
        assertSame(gen1, gen2);
        
        pool.returnObject(gen2);
    }
    
    @Test
    @DisplayName("测试并发借用")
    void testConcurrentBorrow() throws Exception {
        int threadCount = 5;
        java.util.concurrent.CountDownLatch latch = 
            new java.util.concurrent.CountDownLatch(threadCount);
        java.util.concurrent.atomic.AtomicInteger successCount = 
            new java.util.concurrent.atomic.AtomicInteger(0);
        
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                SecurityChainGenerator gen = null;
                try {
                    gen = pool.borrow();
                    // 模拟使用
                    Thread.sleep(100);
                    successCount.incrementAndGet();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (gen != null) {
                        pool.returnObject(gen);
                    }
                    latch.countDown();
                }
            }).start();
        }
        
        latch.await();
        assertEquals(threadCount, successCount.get());
    }
}

代码使用说明

快速开始

  1. 克隆项目并安装依赖:
git clone https://github.com/example/dreamworld-crawler.git
cd dreamworld-crawler
mvn clean install
  1. 准备必要文件:
# 将APK和SO文件放到data目录
mkdir -p data
cp /path/to/dreamworld-app.apk data/
cp /path/to/libSecurityCore.so data/
  1. 运行测试:
mvn test
  1. 启动服务:
mvn spring-boot:run

注意事项

  1. 内存配置:Unidbg需要较大内存,建议JVM配置:

    -Xms512m -Xmx2g
    
  2. 文件路径:确保APK和SO文件路径正确

  3. 并发控制:生产环境建议使用对象池,避免频繁创建模拟器

  4. 错误处理:所有API调用都应该有适当的错误处理和重试机制