核心方案是通过自定义classloader在defineclass前拦截解密字节流,仅加密常量池中带@encrypted注解或enc_前缀的敏感字符串,加载时用启动参数密钥验证hmac并aes解密,解密后立即清空密钥内存,规避框架类、反射类及日志泄露风险。

核心思路是绕过默认类加载路径,用自定义 ClassLoader 在 defineClass 前拦截并解密字节流,让敏感逻辑始终以密文形态落盘、仅在内存中短暂还原为明文。
加密阶段:构建合法但不可读的.class文件
不能整包加密——JVM 会因字节码结构损坏直接拒绝加载。应聚焦于常量池中的敏感内容,例如 API 密钥、SQL 模板、算法参数等字符串字面量:
- 使用 ASM 或 Javassist 扫描编译后的 .class,定位
CONSTANT_Utf8_info条目中带@Encrypted注解或特定前缀(如ENC_)的字符串 - 用 AES-128-CBC 加密该字符串,生成密文 + 16 字节随机 IV,并追加 HMAC-SHA256 签名防篡改
- 将原始字符串替换成 Base64 编码的密文块(如
"ENC_aGVsbG8=|iv:abcd...|hmac:xyz..."),保持 class 文件格式合规
加载阶段:在 findClass 中完成动态解密
继承 ClassLoader,重写 findClass 方法,跳过双亲委派(避免被父加载器提前失败):
- 调用
getResourceAsStream(name.replace('.', '/') + ".class")获取加密字节流 - 用 ASM 解析字节码,遍历常量池,识别出加密标记字段,提取密文、IV 和签名
- 用启动时注入的密钥(如
-Dapp.key=xxx)验证 HMAC,再 AES 解密还原原始字符串 - 构造新字节数组,调用
super.defineClass(name, decryptedBytes, 0, decryptedBytes.length)
密钥与上下文安全:避免硬编码和内存残留
密钥本身是最大风险点,必须与代码分离:
- 禁止写死在源码或配置文件中;推荐通过 JVM 启动参数传入,或从受控环境变量读取
- 解密完成后立即用
Arrays.fill(keyBytes, (byte) 0)清空密钥字节数组 - 若需更高强度,可集成 JNI 调用硬件安全模块(HSM)或 Android Keystore 进行密钥托管
注意事项:规避常见陷阱
实际落地时容易忽略几个关键约束:
- 框架类(如 Spring Bean、Servlet)若被加密,需确保其依赖的类未被加密,否则因类初始化顺序引发
NoClassDefFoundError - 避免加密反射调用目标类、Lambda 生成类或枚举常量——它们的加载时机和方式较特殊
- 日志、堆转储、JFR 记录可能意外暴露解密后字符串,建议配合 JVM 参数
-XX:+IgnoreUnrecognizedVMOptions -XX:+UseContainerSupport并限制敏感字段打印











