
Java混淆隐藏方法具体实现-动态修改字节码的艺术
背景*
近期在利用JADX反编译某封装项目时,发现关键代码方法返回为null,见如下现象截图

可以看见上面截图 createJTF2 方法的方法体为空,并且整个项目只有这一个方法,这么奇怪?
但实际主jar又是可以执行的 他是怎么做到的呢?
进一步解包发现调用处也并没有直接使用代理、自定义类加载等内容
用了什么黑科技?下面我们一起来解密一下
No.1 猜测推断
我在反编译的时候发现 里面存在一个特殊的包 javassist
Javassist(Java Programming Assistant)是一个开源的 Java 字节码编辑库,让你能在运行时或编译时动态修改、生成 Java 类的字节码,而无需深入了解复杂的 JVM 指令和类文件格式。
所以我猜测,方法的逻辑一定是在启动的时候就已经做了字节码的替换,找遍反编译包的资源文件发现了一个比较大的二进制文件,无法打开,猜测所有的字节码应该在里面,因为资源文件只有这一个是最为特殊的!
既然是在启动的时候就已经做了字节码的替换,那么应该能够在代码中找到一些端倪,可惜找遍了所有什么手动代理、自定义classloder等,并没有发现....
此时让我想到了一个特殊的技术可以实现在启动的时候就直接替换字节码加载,他就是Java Agent
一、Java Agent
Java Agent 是一种可以在 JVM 启动后或运行时,动态修改已加载的字节码的技术。它允许你在不修改源代码的情况下,对 Java 应用程序进行监控、增强、调试等操作。
Java Agent 提供了两种挂载方式:Premain(启动时挂载)和 Agentmain(运行时挂载)。它们的核心区别在于挂载时机和能力范围。
1.1 Premain(启动时挂载)
✅ 优势 可以在类首次加载前修改字节码,时机最早,最完整
❌ 劣势 必须重启 JVM,无法在运行时动态挂载
⏰ 时机 JVM 启动时,main 之前工作机制:在 JVM 启动时,main 方法执行之前,通过 -javaagent 参数加载 Agent。
流程是 JVM 启动 → 解析 -javaagent 参数 → 加载 Agent JAR → 调用 premain 方法 → 执行 main 方法
public class PremainAgent {
// 标准入口方法(优先级更高)
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("Premain 启动,参数:" + agentArgs);
inst.addTransformer(new MyTransformer());
}
// 简化版本(如果没有两参数版本会调用这个)
public static void premain(String agentArgs) {
// 没有 Instrumentation 对象,能力受限
}
}
MANIFEST.MF 配置
Premain-Class: PremainAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true如下是启动命令
java -javaagent:/path/to/agent.jar=debugLevel=1 -jar myapp.jar
二、Agentmain(运行时挂载)
✅ 优势 无需重启,可动态挂载到运行中的 JVM
❌ 劣势 已加载的类需要 retransformClasses 才能修改
⏰ 时机 JVM 运行中任意时刻运行机制:JVM 已经运行后,通过 Attach API 动态连接到目标 JVM 进程,注入 Agent。
运行中的 JVM (PID: 12345) ← 通过 Attach API 连接 ← 你的 Attach 程序
↓
发送 load agent 命令
↓
目标 JVM 加载 Agent JAR
↓
调用 agentmain 方法
模版代码如下
public class AgentmainAgent {
// 标准入口方法
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("Agentmain 动态挂载,参数:" + agentArgs);
// 可以重定义已加载的类
inst.addTransformer(new MyTransformer(), true);
try {
// 触发已加载类的重新转换
inst.retransformClasses(SomeClass.class);
} catch (Exception e) {
e.printStackTrace();
}
}
// 简化版本
public static void agentmain(String agentArgs) {
// 没有 Instrumentation 对象
}
}
MANIFEST.MF 配置
Agent-Class: AgentmainAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
动态挂载代码(Attach 端)
import com.sun.tools.attach.*;
public class Attacher {
public static void main(String[] args) throws Exception {
// 1. 获取运行中的 JVM 进程列表
List<VirtualMachineDescriptor> vms = VirtualMachine.list();
// 2. 找到目标进程(比如 PID 为 12345 的进程)
VirtualMachine vm = VirtualMachine.attach("12345");
// 3. 加载 Agent JAR
vm.loadAgent("/path/to/agent.jar", "customArgs");
// 4. 分离
vm.detach();
}
}
根据上面的分析我推测 这个jar一定是在启动的时候 使用了Java Agent 将代理类加到了后面,并在代理类中按照固定的算法或者是规则读取了 这个二进制文件某部分加载了替换了整个类,并且在打包这个JAR的时候用javassist 对相关的类进行字节码的替换将核心逻辑设置为了空逻辑,并按照某规则生成了这个二进制文件,但是在在打包的时候这个加密的老铁 忘记清除javassist 包了.....
No.2 编写代码证实猜想
观看文章的你不用担心 我会在文章末尾放出全部代码的demo
一、新建一个普通的SpringBoot项目引入如下的依赖,以及添加如下的配置application.yml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.29.2-GA</version>
</dependency>
</dependencies>
# 内嵌 Web 服务器配置
server:
# 服务端口号
port: 1122
# 字节码混淆加密配置(BuildTimeProcessor 和 ObfuscateProxy 共用)
obfuscate:
# AES 加密密钥,加密/解密双方必须保持一致
# 这里的加密是我希望我们生成的文件不是普通的class来模拟二进制的文件
aes-key: ObfuscateKey2024
二、编写我们具体工能的类 我这里随便写了两个类做演示(我这里新建了一个注解类被注解标注的方法会被加密混淆)
package com.example.service;
/**
* 用户服务接口
* <p>
* 定义了用户相关的业务方法。ObfuscateProxy 基于此接口创建动态代理,
* 在运行时解密并执行被 @EncryptMethod 标记的真实业务逻辑。
* </p>
*/
public interface IUserService {
/**
* 根据用户ID获取用户信息
*
* @param userId 用户ID
* @return 用户信息字符串
*/
String getUserInfo(String userId);
/**
* 根据数量和单价计算最终价格
*
* @param count 商品数量
* @param price 商品单价
* @return 计算后的最终价格(整数)
*/
int calculatePrice(int count, double price);
}
package com.example.service;
import com.example.annotation.EncryptMethod;
import org.springframework.stereotype.Service;
/**
* 用户服务实现类
* <p>
* 实现了 IUserService 接口,提供具体的业务逻辑。
* 所有带有 @EncryptMethod 注解的方法,其原始字节码会在编译后被 BuildTimeProcessor 加密,
* 方法体被替换为空实现。运行时通过 ObfuscateProxy 动态代理解密真实字节码并执行。
* </p>
*/
@Service
public class UserService implements IUserService {
/**
* 获取用户信息
* <p>
* 根据传入的用户ID拼接用户姓名和年龄信息并返回。
* 该方法被 @EncryptMethod 标记,编译后方法体会被清空,
* 真实逻辑保存在加密文件中,运行时由 ObfuscateProxy 动态解密执行。
* </p>
*
* @param userId 用户ID
* @return 包含用户姓名和年龄的信息字符串
*/
@EncryptMethod
@Override
public String getUserInfo(String userId) {
System.out.println("=== 执行业务逻辑 getUserInfo ===");
String result = "User: " + userId + ", Name: Zhang San, Age: 25";
System.out.println("返回: " + result);
return result;
}
/**
* 计算商品最终价格
* <p>
* 根据商品数量和单价计算总价,若总价超过100则打9折。
* 该方法被 @EncryptMethod 标记,编译后方法体会被清空,
* 真实逻辑保存在加密文件中,运行时由 ObfuscateProxy 动态解密执行。
* </p>
*
* @param count 商品数量
* @param price 商品单价
* @return 折扣后的最终价格(取整)
*/
@EncryptMethod
@Override
public int calculatePrice(int count, double price) {
System.out.println("=== 执行业务逻辑 calculatePrice ===");
System.out.println("数量: " + count + ", 单价: " + price);
double total = count * price;
double discount = total > 100 ? 0.9 : 1.0;
int result = (int) (total * discount);
System.out.println("原价: " + total + ", 折扣: " + discount + ", 最终价: " + result);
return result;
}
}
package com.example.utils;
import com.example.annotation.EncryptMethod;
public class StUtils {
@EncryptMethod
public static String print(String msg){
return "你打印了:"+msg;
}
}
超级普通的三个类 IUserService 、UserService、StUtils,下面是注解类
package com.example.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 加密方法标记注解
* 标记在方法上,表示该方法的真实字节码需要在编译后进行 AES 加密保护。
* 编译阶段,BuildTimeProcessor 会扫描带有此注解的方法,
* 将原始 .class 文件加密存储,并将方法体替换为空实现(返回默认值)。
* 运行时,ObfuscateProxy 会动态解密原始字节码并执行真实的业务逻辑。
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptMethod {
}
三、编写加密逻辑 加密类BuildTimeProcessor、BytecodeEncryptor
package com.example.processor;
import javassist.*;
import java.nio.file.Path;
import java.util.List;
/**
* 编译后字节码混淆加密 — 流程编排类(独立 main 程序)
* 职责:定义并执行加密保护的四步流程,具体实现委托给 {@link BytecodeEncryptor}
* 四步流程:
* 扫描 — 遍历 target/classes 下所有 .class 文件,收集包含 @EncryptMethod 方法的类
* 加密 — 将原始 .class 字节码使用 AES 加密,存储为 .enc 文件到 encrypted/ 目录
* 清空+去注解 — 用 Javassist 将方法体替换为空实现,同时移除方法上的 @EncryptMethod 注解,写回编译产物目录
* 清除痕迹 — 删除 EncryptMethod.class 注解类文件,彻底消除混淆保护痕迹
*
* @see BytecodeEncryptor 具体实现类
*/
public class BuildTimeProcessor {
/**
* 程序入口
* 按顺序执行四步流程:扫描 → 加密 → 清空+去注解 → 清除痕迹
*/
public static void main(String[] args) {
String classesDir = "target/classes";
String encryptedDir = classesDir + "/encrypted";
// 初始化 ClassPool(后续步骤共用)
ClassPool pool = initClassPool(classesDir);
if (pool == null) {
return;
}
// ================================================================
// 第一步:扫描 — 收集所有需要加密保护的 .class 文件
// ================================================================
List<Path> targetClassPaths = step1_scan(classesDir, pool);
if (targetClassPaths.isEmpty()) {
System.out.println("[BuildTimeProcessor] 未找到需要加密的类,流程结束。");
return;
}
System.out.println("[BuildTimeProcessor] 第一步完成:找到 " + targetClassPaths.size() + " 个目标类\n");
// ================================================================
// 第二步:加密 — 将原始字节码 AES 加密后保存为 .enc 文件
// ================================================================
step2_encrypt(targetClassPaths, classesDir, encryptedDir, pool);
System.out.println("[BuildTimeProcessor] 第二步完成:所有目标类已加密\n");
// ================================================================
// 第三步:清空+去注解 — 清空方法体,移除 @EncryptMethod,写回 .class
// ================================================================
step3_emptyAndRemoveAnnotations(targetClassPaths, classesDir, pool);
System.out.println("[BuildTimeProcessor] 第三步完成:方法体已清空,注解已移除\n");
// ================================================================
// 第四步:清除痕迹 — 删除 EncryptMethod.class,彻底消除保护痕迹
// ================================================================
step4_cleanup(classesDir);
System.out.println("[BuildTimeProcessor] 第四步完成:混淆痕迹已清除\n");
System.out.println("========================================");
System.out.println("[BuildTimeProcessor] 混淆加密流程全部完成!");
System.out.println(" - 加密文件目录: " + encryptedDir);
System.out.println(" - 空壳 class 目录: " + classesDir);
System.out.println(" 反编译结果:方法体全为空实现,无保护标记,注解类已消失");
System.out.println("========================================");
}
// ================================================================
// 初始化
// ================================================================
/**
* 初始化 Javassist ClassPool,添加编译产物到搜索路径
*/
private static ClassPool initClassPool(String classesDir) {
ClassPool pool = ClassPool.getDefault();
try {
pool.insertClassPath(classesDir);
return pool;
} catch (NotFoundException e) {
System.err.println("[BuildTimeProcessor] ClassPool 初始化失败: " + e.getMessage());
return null;
}
}
// ================================================================
// 第一步:扫描
// ================================================================
/**
* 第一步 — 扫描编译产物目录
* 遍历 target/classes 下所有 .class 文件,通过 Javassist 分析,
* 收集包含 @EncryptMethod 注解的类(跳过接口、注解、枚举)。
* 具体逻辑委托给 {@link BytecodeEncryptor#scanTargetClasses}
*
* @param classesDir 编译产物根目录
* @param pool Javassist ClassPool
* @return 需要加密保护的 .class 文件路径列表
*/
private static List<Path> step1_scan(String classesDir, ClassPool pool) {
System.out.println("========================================");
System.out.println(" 第一步:扫描目标 .class 文件...");
System.out.println("========================================");
List<Path> targets = BytecodeEncryptor.scanTargetClasses(pool, classesDir);
for (Path path : targets) {
System.out.println(" [✓] 发现目标: " + path);
}
return targets;
}
// ================================================================
// 第二步:加密
// ================================================================
/**
* 第二步 — AES 加密原始字节码
* <p>对第一步收集到的每个目标 .class 文件,读取其完整字节码,
* 使用 AES 加密后保存到 encrypted/ 目录。
* 具体逻辑委托给 {@link BytecodeEncryptor#encryptAndSave}</p>
*
* @param targetClassPaths 第一步收集到的目标 .class 文件列表
* @param classesDir 编译产物根目录
* @param encryptedDir 加密文件存储目录
* @param pool Javassist ClassPool
*/
private static void step2_encrypt(List<Path> targetClassPaths, String classesDir,
String encryptedDir, ClassPool pool) {
System.out.println("========================================");
System.out.println(" 第二步:AES 加密原始字节码...");
System.out.println("========================================");
for (Path path : targetClassPaths) {
BytecodeEncryptor.encryptAndSave(pool, path, classesDir, encryptedDir);
}
}
// ================================================================
// 第三步:清空 + 移除注解
// ================================================================
/**
* 第三步 — 清空方法体并移除 @EncryptMethod 注解
* 对每个目标类,遍历其被 @EncryptMethod 标注的方法:
* 将方法体替换为返回默认值的空实现
* 从字节码中移除 @EncryptMethod 注解(使反编译看不到标记)
* 写回编译产物目录
* 具体逻辑委托给 {@link BytecodeEncryptor#emptyMethodBodiesAndWriteBack}
* @param targetClassPaths 目标 .class 文件列表
* @param classesDir 编译产物根目录
* @param pool Javassist ClassPool
*/
private static void step3_emptyAndRemoveAnnotations(List<Path> targetClassPaths, String classesDir,
ClassPool pool) {
System.out.println("========================================");
System.out.println(" 第三步:清空方法体 + 移除 @EncryptMethod 注解");
System.out.println("========================================");
int totalEmptied = 0;
for (Path path : targetClassPaths) {
int count = BytecodeEncryptor.emptyMethodBodiesAndWriteBack(pool, path, classesDir);
totalEmptied += count;
}
System.out.println(" 共处理 " + totalEmptied + " 个方法(清空+去注解)");
}
// ================================================================
// 第四步:清除痕迹
// ================================================================
/**
* 第四步 — 清除混淆保护痕迹
* 删除 processor 和 annotation 包下的所有 .class 文件,
* 使反编译者完全看不到混淆保护机制的存在。
* 具体逻辑委托给 {@link BytecodeEncryptor#cleanupBuildTimeTraces}
*
* @param classesDir 编译产物根目录
*/
private static void step4_cleanup(String classesDir) {
System.out.println("========================================");
System.out.println(" 第四步:清除混淆痕迹(删除 processor + annotation 包)...");
System.out.println("========================================");
BytecodeEncryptor.cleanupBuildTimeTraces(classesDir);
}
}
package com.example.processor;
import com.example.annotation.EncryptMethod;
import com.example.config.ConfigUtil;
import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.annotation.Annotation;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.lang.reflect.Modifier;
import java.nio.file.*;
import java.util.Base64;
import java.util.List;
import java.util.stream.Collectors;
/**
* 字节码加密工具类
* 提供编译后字节码混淆加密的具体实现方法,
* 由 {@link BuildTimeProcessor} 在编译流程中编排调用。
* 包含以下工具方法:
* 扫描并收集目标 .class 文件
* AES 加密原始字节码并保存为 .enc 文件
* Javassist 清空方法体(替换为返回默认值的空实现)
* 判断方法是否需要加密
*/
public final class BytecodeEncryptor {
/** AES 加密算法 */
private static final String ALGORITHM = "AES";
/** 加密文件的后缀名 */
private static final String ENCRYPTED_FILE_SUFFIX = ".enc";
private BytecodeEncryptor() {
}
// ============================================================
// 第一步:扫描收集目标 .class 文件
// ============================================================
/**
* 扫描 classes 目录,收集所有包含 @EncryptMethod 方法的 .class 文件路径
* 遍历 {classesDir} 下的所有 .class 文件,通过 Javassist 分析每个类,
* 只保留符合以下条件的类:
* 不是接口、注解、枚举
* 包含被 @EncryptMethod 标注的非静态、非私有、非抽象方法
* @param pool Javassist ClassPool 实例
* @param classesDir 编译产物根目录(如 target/classes)
* @return 符合条件的目标 .class 文件路径列表
*/
static List<Path> scanTargetClasses(ClassPool pool, String classesDir) {
try {
return Files.walk(Paths.get(classesDir))
.filter(p -> p.toString().endsWith(".class"))
.filter(p -> !p.toString().replace(File.separator, "/").contains("/encrypted/"))
.filter(p -> hasEncryptableMethods(pool, p, classesDir))
.collect(Collectors.toList());
} catch (IOException e) {
throw new RuntimeException("[BytecodeEncryptor] 扫描目录失败: " + e.getMessage(), e);
}
}
/**
* 判断指定 .class 文件中的类是否包含需要加密的方法
*
* @param pool Javassist ClassPool 实例
* @param path .class 文件路径
* @param classesDir 编译产物根目录
* @return true 表示该类包含需要加密的方法
*/
private static boolean hasEncryptableMethods(ClassPool pool, Path path, String classesDir) {
CtClass ctClass = null;
try {
String className = buildClassName(path, classesDir);
ctClass = pool.get(className);
if (ctClass.isInterface() || ctClass.isAnnotation() || ctClass.isEnum()) {
return false;
}
for (CtMethod method : ctClass.getDeclaredMethods()) {
if (isEncryptableMethod(method)) {
return true;
}
}
return false;
} catch (NotFoundException e) {
return false;
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
}
/**
* 判断单个方法是否需要加密
* 条件:
* 标注了 @EncryptMethod 注解
* 非静态、非私有、非抽象
* @param method Javassist CtMethod 对象
* @return true 表示该方法需要加密保护
*/
private static boolean isEncryptableMethod(CtMethod method) {
try {
if (method.getAnnotation(EncryptMethod.class) == null) {
return false;
}
} catch (ClassNotFoundException e) {
return false;
}
int mod = method.getModifiers();
return !Modifier.isPrivate(mod) && !Modifier.isAbstract(mod);
}
// ============================================================
// 第二步:AES 加密原始字节码
// ============================================================
/**
* 读取 .class 文件的原始字节码,使用 AES 加密后保存到 encrypted 目录
* 加密文件命名规则:
* 类名 "com.example.service.UserService"
* → Base64 URL Safe 编码(无 padding)
* → "Y29tLmV4YW1wbGUuc2VydmljZS5Vc2VyU2VydmljZQ"
* → 文件:encrypted/Y29tLmV4YW1wbGUuc2VydmljZS5Vc2VyU2VydmljZQ.enc
*
* @param pool Javassist ClassPool 实例
* @param path .class 文件的绝对路径
* @param classesDir 编译产物根目录
* @param encryptedDir 加密文件存储目录
* @return 加密文件保存时使用的 key(即类名的 Base64 编码)
*/
static String encryptAndSave(ClassPool pool, Path path, String classesDir, String encryptedDir) {
try {
String className = buildClassName(path, classesDir);
String classKey = classNameToClassKey(className);
byte[] rawBytes = Files.readAllBytes(path);
byte[] encrypted = encrypt(rawBytes);
// 确保加密目录存在
Files.createDirectories(Paths.get(encryptedDir));
try (FileOutputStream fos = new FileOutputStream(
encryptedDir + File.separator + classKey + ENCRYPTED_FILE_SUFFIX)) {
fos.write(encrypted);
}
System.out.println("[BytecodeEncryptor] 加密完成: " + className + " -> " + classKey + ENCRYPTED_FILE_SUFFIX);
return classKey;
} catch (Exception e) {
throw new RuntimeException("[BytecodeEncryptor] 加密失败: " + path + " - " + e.getMessage(), e);
}
}
/**
* AES 加密字节数组
* 密钥通过 {@link ConfigUtil#getKey()} 统一获取,
* 与运行期 {@code ObfuscateProxy.decrypt()} 使用相同密钥。
* @param data 原始字节数组
* @return AES 加密后的字节数组
* @throws Exception 加密异常
*/
public static byte[] encrypt(byte[] data) throws Exception {
byte[] key = ConfigUtil.getKey();
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return cipher.doFinal(data);
}
// ============================================================
// 第三步:清空方法体 + 移除 @EncryptMethod 注解
// ============================================================
/**
* 清空指定类中所有 @EncryptMethod 方法的方法体,同时移除注解
* 遍历类中所有方法,对每个带有 @EncryptMethod 注解的非静态/非私有/非抽象方法:
* 清空方法体(替换为返回默认值的空实现)
* 移除方法上的 @EncryptMethod 注解 — 防止反编译后暴露保护痕迹
* 处理完成后将修改后的类写回编译产物目录。
* @param pool Javassist ClassPool 实例
* @param path .class 文件路径
* @param classesDir 编译产物根目录
* @return 被清空的方法数量
*/
static int emptyMethodBodiesAndWriteBack(ClassPool pool, Path path, String classesDir) {
CtClass ctClass = null;
try {
String className = buildClassName(path, classesDir);
ctClass = pool.get(className);
int count = 0;
for (CtMethod method : ctClass.getDeclaredMethods()) {
if (!isEncryptableMethod(method)) {
continue;
}
emptyMethodBody(method);
removeEncryptMethodAnnotation(method);
System.out.println("[BytecodeEncryptor] 方法体已清空+注解已移除: " + className + "." + method.getName());
count++;
}
ctClass.writeFile(classesDir);
return count;
} catch (Exception e) {
throw new RuntimeException("[BytecodeEncryptor] 清空方法体失败: " + path + " - " + e.getMessage(), e);
} finally {
if (ctClass != null) {
ctClass.detach();
}
}
}
/**
* 从方法上移除 @EncryptMethod 注解
* 通过 Javassist 底层 API 操作字节码属性,
* 删除方法上的 Runtime 可见注解中的 EncryptMethod。
* 反编译工具从此无法看到加密标记。
* @param method Javassist CtMethod 对象
*/
private static void removeEncryptMethodAnnotation(CtMethod method) {
AnnotationsAttribute attr = (AnnotationsAttribute) method.getMethodInfo()
.getAttribute(AnnotationsAttribute.visibleTag);
if (attr != null) {
Annotation[] annotations = attr.getAnnotations();
for (Annotation ann : annotations) {
if (ann.getTypeName().equals(EncryptMethod.class.getName())) {
attr.removeAnnotation(ann.getTypeName());
break;
}
}
}
}
// ============================================================
// 第四步:清除混淆痕迹
// ============================================================
/**
* 删除所有编译期处理器和注解类的 .class 文件
* 打包前从 target/classes 中删除以下包的所有产物,
* 防止反编译后暴露混淆保护机制:
* {@code com/example/processor/} — BuildTimeProcessor、BytecodeEncryptor
* {@code com/example/annotation/} — EncryptMethod 注解
* 这些类仅编译期使用,运行时不需要它们。
* 删除后 JAR 中只会保留加密后的 .enc 文件和空壳业务类,
* 完全看不出使用了何种保护手段。
* </p>
*
* @param classesDir 编译产物根目录
*/
static void cleanupBuildTimeTraces(String classesDir) {
String[] packagesToDelete = {
"com" + File.separator + "example" + File.separator + "processor",
"com" + File.separator + "example" + File.separator + "annotation"
};
for (String pkg : packagesToDelete) {
Path pkgDir = Paths.get(classesDir, pkg);
try {
if (Files.exists(pkgDir)) {
deleteRecursively(pkgDir);
System.out.println("[BytecodeEncryptor] 已删除编译期产物: " + pkgDir);
}
} catch (IOException e) {
System.err.println("[BytecodeEncryptor] 删除失败: " + pkgDir + " - " + e.getMessage());
}
}
}
/**
* 递归删除目录及其所有内容
*/
private static void deleteRecursively(Path dir) throws IOException {
if (Files.isDirectory(dir)) {
try (var entries = Files.list(dir)) {
for (Path entry : entries.toArray(Path[]::new)) {
deleteRecursively(entry);
}
}
}
Files.delete(dir);
}
/**
* 清空单个方法的方法体,替换为返回默认值的空实现
* 根据方法返回类型生成不同的空实现:
* void → 空方法体 {}
* boolean → { return false; }
* long → { return 0L; }
* double → { return 0.0; }
* float → { return 0.0f; }
* int 等其他基本类型 → { return 0; }
* 引用类型(String、Object 等)→ { return null; }
* @param method 需要清空方法体的 Javassist CtMethod 对象
* @throws Exception 方法体替换异常
*/
private static void emptyMethodBody(CtMethod method) throws Exception {
try {
CtClass retType = method.getReturnType();
if (retType == CtClass.voidType) {
method.setBody("{}");
} else if (retType.isPrimitive()) {
String name = retType.getName();
if ("boolean".equals(name)) {
method.setBody("{ return false; }");
} else if ("long".equals(name)) {
method.setBody("{ return 0L; }");
} else if ("double".equals(name)) {
method.setBody("{ return 0.0; }");
} else if ("float".equals(name)) {
method.setBody("{ return 0.0f; }");
} else {
method.setBody("{ return 0; }");
}
} else {
method.setBody("{ return null; }");
}
} catch (CannotCompileException e) {
System.err.println("[BytecodeEncryptor] 无法清空方法 " + method.getName() + ": " + e.getMessage());
}
}
// ============================================================
// 工具方法
// ============================================================
/**
* 从 .class 文件路径推算出 Java 类全限定名
* path = target/classes/com/example/service/UserService.class
* classesDir = target/classes
* → 返回 = "com.example.service.UserService"
* @param path .class 文件路径
* @param classesDir 编译产物根目录
* @return Java 类全限定名
*/
private static String buildClassName(Path path, String classesDir) {
return path.toString()
.substring(classesDir.length() + 1)
.replace(File.separator, ".")
.replace(".class", "");
}
/**
* 将类全限定名转换为加密文件的 key(Base64 URL Safe 编码,无 padding)
*
* @param className Java 类全限定名,如 "com.example.service.UserService"
* @return Base64 URL Safe 编码后的字符串
*/
private static String classNameToClassKey(String className) {
return Base64.getUrlEncoder().withoutPadding()
.encodeToString(className.getBytes());
}
}
四、代码我将演示三种方式对于本Java代码的混淆隐藏加密效果
分别是 JDK代理、ClassLoder、Java Agent 三种方式 下面是三种方式需要用到的类
package com.example.agent;
import org.yaml.snakeyaml.Yaml;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.Base64;
import java.util.Map;
/**
* Java Agent — 方案三:JVM 启动时自动替换加密类
*
* <h3>核心原理:</h3>
* <ol>
* <li>JVM 通过 {@code -javaagent:decrypt-agent.jar} 在启动时加载此 Agent</li>
* <li>{@link #premain} 注册一个 {@link ClassFileTransformer}</li>
* <li>每当 JVM 加载一个类,{@link DecryptTransformer#transform} 被调用</li>
* <li>检查 classpath 中是否存在 {@code /encrypted/类名Base64.enc}</li>
* <li>存在 → AES 解密 → 返回真实字节码(替换空壳类)</li>
* <li>不存在 → 返回 null(使用原始字节码,即空壳类/普通类)</li>
* </ol>
*
* <h3>与前两种方案的区别:</h3>
* <ul>
* <li>无需接口(优于方案一)</li>
* <li>无需自定义 ClassLoader(优于方案二)</li>
* <li>无需修改业务代码(优于方案一和方案二)</li>
* <li>可以 import / new 加密类,完全透明</li>
* <li>JVM 层面拦截,一次解密,之后 JVM 缓存在 Metaspace</li>
* </ul>
*
* <h3>独立打包:</h3>
* <p>此 Agent 已集成到 {@code EncryptInstall} Profile 中,
* 运行 {@code mvn clean package -PEncryptInstall} 时自动产出 {@code target/decrypt-agent.jar}。
* JAR 仅包含 DecryptAgent + SnakeYAML(shaded),不含 Spring Boot,体积极小。</p>
*
* <h3>使用方式:</h3>
* <pre>
* # 一条命令打包(自动产出应用JAR + AgentJAR)
* mvn clean package -PEncryptInstall
*
* # 带 Agent 启动
* java -javaagent:target/decrypt-agent.jar -jar target/obfuscated-demo-1.0-SNAPSHOT.jar
* </pre>
*/
public final class DecryptAgent {
private static final String ALGORITHM = "AES";
private static final String ENCRYPTED_RESOURCE_PREFIX = "encrypted/";
private static final String ENCRYPTED_SUFFIX = ".enc";
private DecryptAgent() {
}
/**
* JVM 启动时回调,注册 ClassFileTransformer
*
* @param agentArgs -javaagent 传入的参数(本实现未使用)
* @param inst Instrumentation 实例
*/
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("[DecryptAgent] ========================================");
System.out.println("[DecryptAgent] Java Agent 解密拦截器已加载");
System.out.println("[DecryptAgent] 注册 ClassFileTransformer...");
System.out.println("[DecryptAgent] ========================================");
inst.addTransformer(new DecryptTransformer(), true);
}
/**
* ClassFileTransformer — 在 JVM 定义类之前拦截并替换字节码
*
* <p>当 JVM 调用 defineClass 之前,此 transform 方法被触发。
* 如果 classpath 中存在该类的 .enc 加密文件,则解密并返回真实字节码;
* 否则返回 null 表示使用原始字节码(走正常加载流程)。</p>
*/
private static class DecryptTransformer implements ClassFileTransformer {
@Override
public byte[] transform(Module module, ClassLoader loader, String className,
Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
try {
String encResourcePath = buildEncResourcePath(className);
// 尝试通过当前 ClassLoader 读取 .enc 加密资源
InputStream encStream = findEncResource(loader, encResourcePath);
if (encStream == null) {
return null;
}
System.out.println("[DecryptAgent] 拦截并解密: " + className.replace('/', '.'));
byte[] encrypted = encStream.readAllBytes();
encStream.close();
return decrypt(encrypted);
} catch (Exception e) {
System.err.println("[DecryptAgent] 解密失败 " + className + ": " + e.getMessage());
return null;
}
}
/**
* 将 JVM 内部类名转换为 .enc 资源路径
*
* <p>注意:JVM 传的是斜杠格式(com/example/service/UserService),
* 但 BuildTimeProcessor 加密时用的是点号格式(com.example.service.UserService)。
* 因此必须先将 '/' 替换为 '.' 再 Base64 编码,才能匹配 .enc 文件名。</p>
*
* <p>例:{@code com/example/service/UserService}
* → 转点 → {@code com.example.service.UserService}
* → Base64URL → {@code Y29tLmV4YW1wbGUuc2VydmljZS5Vc2VyU2VydmljZQ}
* → 路径 → {@code encrypted/Y29tLmV4YW1wbGUuc2VydmljZS5Vc2VyU2VydmljZQ.enc}</p>
*/
private String buildEncResourcePath(String className) {
String dotName = className.replace('/', '.');
String classKey = Base64.getUrlEncoder().withoutPadding()
.encodeToString(dotName.getBytes());
return ENCRYPTED_RESOURCE_PREFIX + classKey + ENCRYPTED_SUFFIX;
}
/**
* 多级回退查找 .enc 资源
* <ol>
* <li>当前加载该类的 ClassLoader(JAR 模式:BOOT-INF/classes/encrypted/)</li>
* <li>System ClassLoader(IDE 模式:target/classes/encrypted/)</li>
* <li>Agent 自身的 ClassLoader(兜底)</li>
* </ol>
*/
private InputStream findEncResource(ClassLoader loader, String path) {
// 1. 当前类加载器(Spring Boot fat JAR 的 LaunchedURLClassLoader)
if (loader != null) {
InputStream is = loader.getResourceAsStream(path);
if (is != null) {
return is;
}
}
// 2. System ClassLoader(IDE classpath 模式)
InputStream is = ClassLoader.getSystemResourceAsStream(path);
if (is != null) {
return is;
}
// 3. Agent 自身的 ClassLoader(兜底)
return DecryptAgent.class.getClassLoader().getResourceAsStream(path);
}
}
// ================================================================
// AES 解密 & 密钥读取(不依赖 ConfigUtil,Agent 独立运行)
// ================================================================
static byte[] decrypt(byte[] data) throws Exception {
byte[] key = getKey();
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(data);
}
private static byte[] getKey() {
if (cachedKey != null) {
return cachedKey;
}
synchronized (DecryptAgent.class) {
if (cachedKey != null) {
return cachedKey;
}
cachedKey = loadKeyFromYaml();
return cachedKey;
}
}
private static volatile byte[] cachedKey;
@SuppressWarnings("unchecked")
private static byte[] loadKeyFromYaml() {
// 多级回退查找 application.yml
String[] configPaths = {"/application.yml", "application.yml"};
for (String path : configPaths) {
try (InputStream is = DecryptAgent.class.getResourceAsStream(path)) {
if (is == null) {
continue;
}
Yaml yaml = new Yaml();
Map<String, Object> root = yaml.load(is);
if (root == null) {
continue;
}
Map<String, Object> obfuscate = (Map<String, Object>) root.get("obfuscate");
if (obfuscate == null) {
continue;
}
Object keyObj = obfuscate.get("aes-key");
if (keyObj != null) {
return keyObj.toString().getBytes();
}
} catch (Exception ignored) {
}
}
throw new IllegalStateException(
"[DecryptAgent] 无法从 classpath 读取 application.yml 中的 obfuscate.aes-key");
}
}
package com.example.config;
import org.yaml.snakeyaml.Yaml;
import java.io.InputStream;
import java.util.Map;
/**
* 混淆配置工具类
* <p>
* 从 classpath 中的 application.yml 读取 obfuscate 相关配置,
* 为 BuildTimeProcessor(编译期加密)和 ObfuscateProxy(运行期解密)
* 提供统一的 AES 密钥来源。
* </p>
*
* <p>密钥只加载一次并缓存,避免重复 IO。</p>
*/
public final class ConfigUtil {
/** application.yml 在 classpath 中的路径 */
private static final String CONFIG_FILE = "/application.yml";
/** YML 中 obfuscate 配置节点的 key */
private static final String KEY_OBFUSCATE = "obfuscate";
/** YML 中 AES 密钥的 key */
private static final String KEY_AES_KEY = "aes-key";
/** 缓存的 AES 密钥字节数组,首次访问时从配置文件加载 */
private static volatile byte[] cachedKey;
private ConfigUtil() {
}
/**
* 获取 AES 加密/解密密钥(字节数组形式)
* <p>
* 首次调用时从 classpath:/application.yml 中读取 obfuscate.aes-key 配置,
* 后续调用直接返回缓存值。
* </p>
*
* @return AES 密钥的字节数组
* @throws IllegalStateException 如果配置文件不存在或密钥未配置
*/
public static byte[] getKey() {
if (cachedKey != null) {
return cachedKey;
}
synchronized (ConfigUtil.class) {
if (cachedKey != null) {
return cachedKey;
}
cachedKey = loadKeyFromYaml();
return cachedKey;
}
}
/**
* 从 application.yml 中加载 AES 密钥
*
* @return 密钥字节数组
*/
@SuppressWarnings("unchecked")
private static byte[] loadKeyFromYaml() {
try (InputStream is = ConfigUtil.class.getResourceAsStream(CONFIG_FILE)) {
if (is == null) {
throw new IllegalStateException(
"配置文件 " + CONFIG_FILE + " 未找到,请确保 application.yml 在 classpath 中");
}
Yaml yaml = new Yaml();
Map<String, Object> root = yaml.load(is);
if (root == null) {
throw new IllegalStateException("配置文件 " + CONFIG_FILE + " 内容为空");
}
Map<String, Object> obfuscate = (Map<String, Object>) root.get(KEY_OBFUSCATE);
if (obfuscate == null) {
throw new IllegalStateException(
"配置文件中未找到 " + KEY_OBFUSCATE + " 节点,请在 application.yml 中添加 obfuscate.aes-key");
}
Object keyObj = obfuscate.get(KEY_AES_KEY);
if (keyObj == null) {
throw new IllegalStateException(
"配置文件中未找到 " + KEY_AES_KEY + ",请在 application.yml 中设置 obfuscate.aes-key");
}
return keyObj.toString().getBytes();
} catch (Exception e) {
throw new IllegalStateException("读取 AES 密钥失败: " + e.getMessage(), e);
}
}
}
package com.example.core;
import com.example.config.ConfigUtil;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* FileClassLoader — 从 classpath 加密资源 (.enc) 加载真实类
*
* <h3>工作原理:</h3>
* <ol>
* <li>重写 loadClass,打破双亲委派</li>
* <li>检测 classpath 上是否存在 /encrypted/类名Base64.enc</li>
* <li>存在 → 用父 ClassLoader 的 getResourceAsStream 读取 → AES 解密 → defineClass</li>
* <li>不存在 → 走标准双亲委派</li>
* </ol>
*
* <h3>关键:不再依赖文件系统路径</h3>
* <p>.enc 文件通过 classpath 资源读取(getResourceAsStream),
* IDE 中 target/classes 和 JAR 中 BOOT-INF/classes 都能正确找到。</p>
*
* <h3>使用方式:</h3>
* <pre>
* FileClassLoader cl = new FileClassLoader(DemoApplication.class.getClassLoader());
* Class<?> realCls = cl.loadClass("com.example.service.UserService");
* Object svc = realCls.getDeclaredConstructor().newInstance();
* String result = (String) realCls.getMethod("getUserInfo", String.class).invoke(svc, "001");
* </pre>
*
* <h3>注意事项:</h3>
* <ul>
* <li>不能 import / new 加密类(AppClassLoader 会先加载空壳)</li>
* <li>始终通过 FileClassLoader.loadClass() + 反射获取真实类实例</li>
* </ul>
*/
public class FileClassLoader extends ClassLoader {
private static final String ALGORITHM = "AES";
private static final String ENCRYPTED_RESOURCE_PREFIX = "/encrypted/";
private static final String ENCRYPTED_SUFFIX = ".enc";
/** 已解密的类缓存 */
private static final Map<String, Class<?>> DECRYPTED_CLASS_CACHE = new ConcurrentHashMap<>();
public FileClassLoader(ClassLoader parent) {
super(parent);
}
// ================================================================
// 核心:重写 loadClass,打破双亲委派
// ================================================================
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 缓存命中
Class<?> cached = DECRYPTED_CLASS_CACHE.get(name);
if (cached != null) {
return cached;
}
// JVM 核心类交给父加载器
if (name.startsWith("java.") || name.startsWith("javax.")
|| name.startsWith("sun.") || name.startsWith("jdk.")
|| name.startsWith("org.springframework.")) {
return super.loadClass(name, resolve);
}
// 计算 classpath 上的 .enc 资源路径
String encResourcePath = buildEncResourcePath(name);
// 尝试通过父 ClassLoader 的 classpath 读取 .enc 资源
// IDE 模式:从 target/classes/encrypted/ 读取
// JAR 模式:从 BOOT-INF/classes/encrypted/ 读取
try (InputStream encStream = getParent().getResourceAsStream(encResourcePath)) {
if (encStream != null) {
// 【加密模式】.enc 存在 → 解密 → defineClass
byte[] encrypted = encStream.readAllBytes();
byte[] decrypted = decrypt(encrypted);
Class<?> clazz = defineClass(name, decrypted, 0, decrypted.length);
if (resolve) {
resolveClass(clazz);
}
DECRYPTED_CLASS_CACHE.put(name, clazz);
System.out.println("[FileClassLoader] 已从 classpath 加密资源加载: " + name);
return clazz;
}
} catch (Exception e) {
throw new ClassNotFoundException(
"解密类失败: " + name + " (资源路径: " + encResourcePath + ")", e);
}
// 【普通模式】无 .enc → 走标准双亲委派
System.out.println("[FileClassLoader] 无 .enc,走父加载器: " + name);
return super.loadClass(name, resolve);
}
// ================================================================
// 加密资源路径 & 解密
// ================================================================
/**
* 类名 → classpath 上的 .enc 资源路径
* <p>
* 例:com.example.service.UserService
* → /encrypted/Y29tLmV4YW1wbGUuc2VydmljZS5Vc2VyU2VydmljZQ.enc
* </p>
*/
private String buildEncResourcePath(String className) {
String classKey = Base64.getUrlEncoder().withoutPadding()
.encodeToString(className.getBytes());
return ENCRYPTED_RESOURCE_PREFIX + classKey + ENCRYPTED_SUFFIX;
}
private byte[] decrypt(byte[] data) throws Exception {
byte[] key = ConfigUtil.getKey();
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(data);
}
// ================================================================
// 测试入口
// ================================================================
public static void main(String[] args) throws Exception {
FileClassLoader cl = new FileClassLoader(
FileClassLoader.class.getClassLoader());
Class<?> realUserService = cl.loadClass("com.example.service.UserService");
Object instance = realUserService.getDeclaredConstructor().newInstance();
System.out.println("类加载器: " + instance.getClass().getClassLoader());
Object result = realUserService.getMethod("getUserInfo", String.class)
.invoke(instance, "001");
System.out.println("getUserInfo 结果: " + result);
}
}
package com.example.core;
import com.example.config.ConfigUtil;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 混淆代理处理器
* <p>
* 基于 JDK 动态代理,在运行时拦截被 @EncryptMethod 标记的方法调用。
* 当代理方法被调用时,从 classpath 中读取加密的原始 .class 字节码,
* 使用 AES 解密后通过自定义 ClassLoader 加载,并反射调用真实方法。
* </p>
*
* <p>工作流程:</p>
* <ol>
* <li>编译阶段:BuildTimeProcessor 将原始 .class 加密为 .enc 文件,
* 并清空方法体</li>
* <li>运行时:ObfuscateProxy 拦截方法调用,解密 .enc 文件获取真实字节码</li>
* <li>通过 ObfuscatedClassLoader 加载解密后的类</li>
* <li>反射调用真实方法并返回结果</li>
* </ol>
*
* <p>使用方式:</p>
* <pre>
* {@code
* UserService raw = ctx.getBean(UserService.class);
* IUserService svc = ObfuscateProxy.create(IUserService.class, raw);
* svc.getUserInfo("001"); // 自动解密并执行真实逻辑
* }
* </pre>
*/
public class ObfuscateProxy implements InvocationHandler {
/** AES 加密算法 */
private static final String ALGORITHM = "AES";
/** 已解密加载的真实类缓存,key 为类全限定名,避免重复解密 */
private static final Map<String, Class<?>> REAL_CLASS_CACHE = new ConcurrentHashMap<>();
/** 被代理的目标对象(方法体已被清空的 Spring Bean) */
private final Object target;
/**
* 私有构造方法,通过 {@link #create(Class, Object)} 工厂方法创建实例
*
* @param target 被代理的目标对象
*/
private ObfuscateProxy(Object target) {
this.target = target;
}
/**
* 创建混淆代理对象
* <p>
* 为目标对象创建一个 JDK 动态代理,代理实现了指定接口。
* 当调用代理对象的方法时,会触发 {@link #invoke(Object, Method, Object[])} 进行拦截处理。
* </p>
*
* @param <T> 接口类型
* @param iface 目标接口的 Class 对象
* @param target 被代理的目标对象实例
* @return 实现了指定接口的代理对象
*/
@SuppressWarnings("unchecked")
public static <T> T create(Class<T> iface, T target) {
return (T) Proxy.newProxyInstance(
iface.getClassLoader(),
new Class<?>[]{iface},
new ObfuscateProxy(target)
);
}
/**
* 代理方法调用拦截
* <p>
* 当代理对象的任意方法被调用时触发此方法。处理逻辑如下:
* </p>
* <ol>
* <li>如果方法是 Object 类的方法(如 toString、hashCode),直接委托给目标对象</li>
* <li>尝试获取真实类(解密后的原始字节码),如果获取失败则退回目标对象</li>
* <li>使用真实类创建新实例,反射调用同名方法并返回结果</li>
* </ol>
*
* @param proxy 代理对象
* @param method 被调用的方法
* @param args 方法参数
* @return 方法执行结果
* @throws Throwable 方法执行过程中可能抛出的任何异常
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Object 类的方法直接委托给目标对象执行
if (method.getDeclaringClass() == Object.class) {
return method.invoke(target, args);
}
// 尝试获取解密后的真实类
Class<?> realClass = getRealClass(method);
if (realClass == null) {
// 未找到加密文件,退回目标对象的方法体(空实现)
return method.invoke(target, args);
}
// 创建真实类的实例并通过反射调用真实方法
Object instance = realClass.getDeclaredConstructor().newInstance();
return realClass.getMethod(method.getName(), method.getParameterTypes()).invoke(instance, args);
}
/**
* 通过目标类名获取解密后的真实类
* <p>
* 根据目标对象的类名,计算对应的加密文件路径(/encrypted/类名的Base64编码.enc),
* 从 classpath 中读取加密文件,AES 解密后通过自定义 ClassLoader 加载。
* 解密后的 Class 会被缓存到 {@link #REAL_CLASS_CACHE} 中以避免重复解密。
* </p>
*
* @param method 被调用的方法(用于获取所属类的信息)
* @return 解密后的真实类,如果加密文件不存在或解密失败则返回 null
*/
private Class<?> getRealClass(Method method) {
String className = target.getClass().getName();
return REAL_CLASS_CACHE.computeIfAbsent(className, k -> {
try {
// 类名的 Base64 URL 安全编码(与 BuildTimeProcessor 保持一致)
String classKey = Base64.getUrlEncoder().withoutPadding()
.encodeToString(className.getBytes());
String path = "/encrypted/" + classKey + ".enc";
// 从 classpath 读取加密文件
try (InputStream is = getClass().getResourceAsStream(path)) {
if (is == null) {
return null;
}
byte[] encrypted = is.readAllBytes();
byte[] decrypted = decrypt(encrypted);
// 使用自定义 ClassLoader 加载解密后的字节码
ObfuscatedClassLoader loader = new ObfuscatedClassLoader(
getClass().getClassLoader());
return loader.loadFromBytes(className, decrypted);
}
} catch (Exception e) {
return null;
}
});
}
/**
* AES 解密字节数组
*
* @param data 加密的字节数组
* @return 解密后的字节数组
* @throws Exception 解密过程中可能抛出的异常
*/
static byte[] decrypt(byte[] data) throws Exception {
byte[] key = ConfigUtil.getKey();
SecretKeySpec keySpec = new SecretKeySpec(key, ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return cipher.doFinal(data);
}
/**
* 混淆类加载器
* <p>
* 自定义 ClassLoader,用于直接从字节数组加载类定义。
* 这样可以在运行时动态加载解密后的原始 .class 文件,
* 而不会与 Spring 容器中方法体已被清空的类冲突。
* </p>
*/
private static class ObfuscatedClassLoader extends ClassLoader {
/**
* 构造方法,指定父类加载器
*
* @param parent 父类加载器
*/
ObfuscatedClassLoader(ClassLoader parent) {
super(parent);
}
/**
* 从字节数组加载类
* <p>
* 调用 ClassLoader 的 defineClass 方法将字节数组转换为 JVM 可识别的 Class 对象。
* </p>
*
* @param name 类的全限定名
* @param bytes 类的字节码
* @return 加载后的 Class 对象
*/
Class<?> loadFromBytes(String name, byte[] bytes) {
return defineClass(name, bytes, 0, bytes.length);
}
}
}
五、编写启动类以及Maven的打包配置
注意:需要在Java目录新建一个agent.xml 来满足Maven插件动态打包的配置
<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0
http://maven.apache.org/xsd/assembly-2.2.0.xsd">
<id>agent</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<!-- Agent 自身类 + application.yml(从 target/classes 目录直读) -->
<fileSets>
<fileSet>
<directory>${project.build.outputDirectory}</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>com/example/agent/**</include>
<include>application.yml</include>
</includes>
</fileSet>
</fileSets>
<!-- SnakeYAML 依赖(解包到根目录) -->
<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<unpack>true</unpack>
<unpackOptions>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/LICENSE*</exclude>
<exclude>META-INF/NOTICE*</exclude>
<exclude>META-INF/DEPENDENCIES</exclude>
<exclude>META-INF/maven/**</exclude>
<exclude>META-INF/versions/**</exclude>
<exclude>module-info.class</exclude>
</excludes>
</unpackOptions>
<includes>
<include>org.yaml:snakeyaml</include>
</includes>
</dependencySet>
</dependencySets>
</assembly>
package com.example;
import com.example.core.FileClassLoader;
import com.example.core.ObfuscateProxy;
import com.example.service.IUserService;
import com.example.service.UserService;
import com.example.utils.StUtils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
/**
* Spring Boot 应用程序入口 — 演示三种字节码加密保护的运行方案
*
* <h3>运行前提:</h3>
* <ol>
* <li>先执行 {@code mvn clean package -PEncryptInstall} 生成加密 .enc 文件</li>
* <li>确保 target/classes/encrypted/ 下有 .enc 文件</li>
* </ol>
*
* <h3>三种方案对比:</h3>
* <table>
* <tr><th>方案</th><th>入口方法</th><th>原理</th><th>侵入性</th><th>性能</th><th>额外步骤</th></tr>
* <tr>
* <td>JDK 动态代理</td><td>{@link #demoProxy}</td>
* <td>代理拦截方法调用 → 解密 .enc → 反射执行</td>
* <td>需要接口 + ObfuscateProxy.create()</td>
* <td>每次调用有反射损耗</td>
* <td>无</td>
* </tr>
* <tr>
* <td>FileClassLoader</td><td>{@link #demoClassLoader}</td>
* <td>类加载时解密 .enc → defineClass(真实字节码)</td>
* <td>不能 import 加密类,必须反射</td>
* <td>仅加载时解密一次,运行时零损耗</td>
* <td>无</td>
* </tr>
* <tr>
* <td><b>Java Agent</b> ★推荐</td><td>{@link #demoAgent}</td>
* <td>JVM 加载类时自动拦截 → 解密替换字节码</td>
* <td><b>零侵入!可 import/new,代码完全正常</b></td>
* <td>同类加载时解密一次,运行时零损耗</td>
* <td>需 -javaagent 启动参数</td>
* </tr>
* </table>
*
* <h3>方案三(Agent)的打包与运行:</h3>
* <pre>
* # 一条命令:加密 + 打包应用JAR + 打包AgentJAR
* mvn clean package -PEncryptInstall
*
* # 带 Agent 启动(零侵入!)
* java -javaagent:target/decrypt-agent.jar -jar target/obfuscated-demo-1.0-SNAPSHOT.jar
* </pre>
*/
@SpringBootApplication
public class DemoApplication {
/**
* 应用程序入口。
*
* <p>默认演示方案二(FileClassLoader),可直接运行。
* 要使用方案三(Agent),请按上述步骤打包并用 -javaagent 启动,
* 然后取消 demoAgent() 的注释。</p>
*
* <p>快速切换:</p>
* <ul>
* <li>方案一:取消 demoProxy(ctx) 注释</li>
* <li>方案二:取消 demoClassLoader() 注释(当前默认)</li>
* <li>方案三:用 -javaagent 启动 + 取消 demoAgent(ctx) 注释</li>
* </ul>
*/
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext ctx = SpringApplication.run(DemoApplication.class, args);
// ================================================================
// 方案一:JDK 动态代理(需要接口 + ObfuscateProxy)
// ================================================================
// demoProxy(ctx);
// ================================================================
// 方案二:FileClassLoader 打破双亲委派(不需要接口,但必须用反射)
// ================================================================
// demoClassLoader();
// ================================================================
// 方案三:Java Agent(★推荐 — 零侵入,代码完全正常)
// 使用前需:java -javaagent:target/decrypt-agent.jar -jar ...
// ================================================================
demoAgent(ctx);
ctx.close();
}
// ================================================================
// 方案一:JDK 动态代理
// ================================================================
/**
* JDK 动态代理方案
*
* <p>流程:</p>
* <ol>
* <li>从 Spring 容器拿到空壳 Bean(方法体已被清空为 null/0)</li>
* <li>ObfuscateProxy.create() 创建 JDK 动态代理</li>
* <li>代理拦截每个方法调用 → 从 classpath 读取 .enc → AES 解密 → 反射调用真实逻辑</li>
* </ol>
*
* <p>优点:代码简单,有接口就能用,直接 import 加密类</p>
* <p>缺点:每次调用都走反射,有性能损耗</p>
*/
private static void demoProxy(ConfigurableApplicationContext ctx) {
System.out.println("========================================");
System.out.println(" 方案一:JDK 动态代理方案");
System.out.println("========================================");
System.out.println();
UserService raw = ctx.getBean(UserService.class);
IUserService svc = ObfuscateProxy.create(IUserService.class, raw);
System.out.println("=== 测试 getUserInfo ===");
System.out.println("结果: " + svc.getUserInfo("001"));
System.out.println();
System.out.println("=== 测试 calculatePrice ===");
System.out.println("结果: " + svc.calculatePrice(3, 50));
System.out.println("[方案一] JDK 代理演示完成");
}
// ================================================================
// 方案二:FileClassLoader 打破双亲委派
// ================================================================
/**
* FileClassLoader 方案
*
* <p>流程:</p>
* <ol>
* <li>创建 FileClassLoader,指向父 ClassLoader</li>
* <li>loadClass() 时检测 .enc 文件</li>
* <li>有 .enc → AES 解密 → defineClass(真实字节码),跳过空壳 .class</li>
* <li>没有 .enc → 走标准双亲委派</li>
* </ol>
*
* <p>优点:仅加载时解密一次,运行时零反射损耗,不需要接口</p>
* <p>缺点:不能 import / new 加密类(AppClassLoader 会先加载空壳),必须用反射调用</p>
*/
private static void demoClassLoader() throws Exception {
System.out.println("========================================");
System.out.println(" 方案二:FileClassLoader 方案");
System.out.println("========================================");
System.out.println();
FileClassLoader cl = new FileClassLoader(DemoApplication.class.getClassLoader());
Class<?> realUserService = cl.loadClass("com.example.service.UserService");
Object svc = realUserService.getDeclaredConstructor().newInstance();
System.out.println("=== 测试 getUserInfo ===");
String result1 = (String) realUserService
.getMethod("getUserInfo", String.class)
.invoke(svc, "001");
System.out.println("结果: " + result1);
System.out.println();
System.out.println("=== 测试 calculatePrice ===");
int result2 = (int) realUserService
.getMethod("calculatePrice", int.class, double.class)
.invoke(svc, 3, 50);
System.out.println("结果: " + result2);
System.out.println();
System.out.println("[方案二] FileClassLoader 演示完成");
}
// ================================================================
// 方案三:Java Agent(★推荐)
// ================================================================
/**
* Java Agent 方案 — 零侵入,完全透明的解密方案
*
* <h3>原理:</h3>
* <p>JVM 启动时通过 {@code -javaagent:decrypt-agent.jar} 加载
* {@link com.example.agent.DecryptAgent},
* 注册 {@link java.lang.instrument.ClassFileTransformer},
* 在 JVM 定义每个类之前检查是否存在对应的 .enc 加密文件,
* 如有则自动解密并替换字节码。</p>
*
* <h3>与方案一、二的根本区别:</h3>
* <ul>
* <li>不需要接口(优于方案一)</li>
* <li>不需要自定义 ClassLoader(优于方案二)</li>
* <li>不需要反射调用(优于方案二)</li>
* <li><b>业务代码完全不需要修改!</b></li>
* <li>可以正常 import、new、@Autowired 加密类</li>
* <li>JVM 层面拦截,代码零感知</li>
* </ul>
*
* <h3>前置条件:</h3>
* <ol>
* <li>执行 {@code mvn clean package -PEncryptInstall}(一条命令完成加密+双JAR)</li>
* <li>用 {@code java -javaagent:target/decrypt-agent.jar -jar target/obfuscated-demo-1.0-SNAPSHOT.jar} 启动</li>
* </ol>
*
* <p>优点:零侵入、零反射、完全透明、性能最优</p>
* <p>缺点:需要 -javaagent 启动参数(部署时多一个启动参数)</p>
*/
private static void demoAgent(ConfigurableApplicationContext ctx) {
System.out.println("========================================");
System.out.println(" 方案三:Java Agent 方案(零侵入)");
System.out.println("========================================");
System.out.println();
// ★ 关键:业务代码完全正常,不需要任何代理或自定义 ClassLoader!
// JVM 在加载 UserService 时,DecryptAgent 已自动将空壳字节码替换为真实字节码。
// 这里拿到的就是完整业务逻辑的 Bean。
UserService svc = ctx.getBean(UserService.class);
System.out.println("=== 测试 getUserInfo ===");
System.out.println("结果: " + svc.getUserInfo("001"));
System.out.println();
System.out.println("=== 测试 calculatePrice ===");
System.out.println("结果: " + svc.calculatePrice(3, 50));
System.out.println();
System.out.println("测试方法2");
System.out.println(StUtils.print("张三伪装的大佬"));
System.out.println("[方案三] Java Agent 演示完成");
System.out.println(" 注意:如看到默认值(null/0),说明未用 -javaagent 启动");
}
}
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- ================================================================
EncryptInstall Profile — 一键完成:加密 + 打包应用JAR + 打包AgentJAR
产物:
target/obfuscated-demo-1.0-SNAPSHOT.jar ← 应用 fat JAR
target/decrypt-agent.jar ← Agent JAR(~300KB)
流程:
1. compile — 编译所有 Java 源码
2. process-classes — BuildTimeProcessor 四步加密
3. package — spring-boot:repackage + assembly:single
运行: mvn clean package -PEncryptInstall
================================================================ -->
<profiles>
<profile>
<id>EncryptInstall</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<excludes>
<exclude>com/example/agent/**</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>encrypt-bytecode</id>
<phase>process-classes</phase>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>com.example.processor.BuildTimeProcessor</mainClass>
<classpathScope>compile</classpathScope>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.6.0</version>
<configuration>
<descriptors>
<descriptor>src/main/assembly/agent.xml</descriptor>
</descriptors>
<finalName>decrypt-agent</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Premain-Class>com.example.agent.DecryptAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
</configuration>
<executions>
<execution>
<id>build-agent-jar</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
完成上面的配置之后刷新Maven 会在Maven中看到插件直接安装即可

打包完成后直接在项目更目录运行
java -javaagent:target/decrypt-agent.jar -jar target/obfuscated-demo-1.0-SNAPSHOT.jar


可以发现打印是正常的,我们用反编译工具JADX反编译这个JAR看看能看到源码吗


可以看到,源码为空
整个源码以及MD的说明我放到了我的git需要的朋友可以直接参考
git仓库:点我跳转

发表评论