核心是仅加密常量池中带@encrypted注解或enc_前缀的敏感字符串,用aes-128-cbc加随机iv和hmac-sha256签名,替换为base64密文块;自定义classloader在findclass中解析字节码、验证签名、解密并立即清空密钥内存。

核心是让敏感类在磁盘上始终以密文存在,仅在 JVM 加载瞬间解密并进入内存,且明文生命周期极短。不能整包加密 class 文件,必须聚焦常量池中真正的敏感内容——比如 API 密钥、SQL 模板、算法参数、硬编码 Token 等字符串字面量。
只加密常量池里的敏感字符串,不碰字节码结构
整文件 AES 加密会导致 JVM 无法识别 ClassFile 格式,直接抛 NoClassDefFoundError。正确做法是用 ASM 或 Javassist 扫描编译后的 .class,在常量池(Constant Pool)中定位带 @Encrypted 注解或以 ENC_ 开头的 CONSTANT_Utf8_info 条目:
- 将原始字符串(如
"sk_test_xxx")替换成 Base64 编码的密文块,格式类似"ENC_aGVsbG8=|iv:abcd|hmac:xyz" - 密文由 AES-128-CBC 加密生成,每次加密附带 16 字节随机 IV
- 追加 HMAC-SHA256 签名,防止篡改;签名密钥与解密密钥物理分离
- 替换后 class 文件仍能通过 JVM 验证,可正常被
javap查看,但敏感值不可读
自定义加载器在 findClass 中完成动态解密
继承 ClassLoader,重写 findClass,跳过双亲委派,直接接管目标类的字节流获取与解密逻辑:
- 调用
getResourceAsStream(name.replace('.', '/') + ".class")读取加密字节流 - 用 ASM
ClassReader解析,遍历常量池,识别出ENC_标记项 - 提取密文、IV 和 HMAC,先验证签名,再 AES 解密还原原始字符串
- 构造新字节数组,调用
super.defineClass(name, decryptedBytes, 0, len) - 解密完成后立即用
Arrays.fill(keyBytes, (byte) 0)清空密钥内存
密钥必须与代码完全隔离
密钥是整个方案最脆弱的一环,任何硬编码都会让保护形同虚设:
- 禁止出现在源码、配置文件、jar 包资源中;推荐通过 JVM 启动参数注入,例如
-Dapp.key=xxx - 生产环境可对接 KMS 或 HSM,Android 上优先使用 Keystore
- 若需多环境支持,按环境变量区分密钥(如
CLASS_ENCRYPT_KEY_PROD),启动时动态加载 - 避免日志打印密钥、堆转储泄露 keyBytes,建议配合
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/safe/path并限制 dump 范围
绕开框架和反射的加载陷阱
Spring、Hibernate、Servlet 容器等会提前加载大量类,稍有不慎就会触发类初始化失败:
- 不要加密 Spring Bean 类本身,但可加密其内部的
static final String字段 - 避开枚举类、Lambda 生成类、
Unsafe相关类——它们的加载路径特殊,易引发IncompatibleClassChangeError - 确保被加密字段所在类的依赖类(如工具类、DTO)未被加密,否则双亲委派中断后找不到父类
- 禁用框架的类缓存机制(如 Spring 的
ConfigurationClassPostProcessor缓存),防止旧字节码残留











