首页
学习
活动
专区
圈层
工具
发布

SpringBoot 的 Jar 包到底怎么加密?别再把混淆当加密了

把 SpringBoot 项目打成 Jar 包交付给客户,别人执行一条命令,代码基本就摊开了:

jar -xf billing-service.jar

javap -c BOOT-INF/classes/com/demo/billing/PriceService.class

类名、方法名、SQL、接口地址,甚至某些写死的密钥,都能翻出来。

这个地方我一直不太信所谓的“Jar 一键加密”。JVM 最终必须拿到可执行的字节码,只要代码能运行,就一定存在被抓取的可能。我们能做的不是让它永远无法反编译,而是把分析成本抬高,别让人解压后五分钟就看完核心逻辑。

普通项目我一般分三层处理:代码混淆、核心类加密、敏感配置外置。

先处理混淆。

混淆会改掉类名、方法名和局部变量名。例如原来的:

public BigDecimal calculateContractPrice(Long customerId, BigDecimal amount) {

  return discountRepository.findRate(customerId)

          .map(amount::multiply)

          .orElse(amount);

}

处理后,反编译出来可能只剩下a.a(Long, BigDecimal)。逻辑仍然能看,但阅读成本已经高了不少。

不过 Spring 项目不能一股脑全改。Controller、Entity、配置类以及被反射调用的方法,乱改后项目大概率启动失败:

NoSuchBeanDefinitionException: No qualifying bean of type 'PricePolicy'

JsonMappingException: No serializer found for class CustomerQuote

所以混淆规则里至少要保留这些内容:

-keep @org.springframework.boot.autoconfigure.SpringBootApplication class *

-keep @org.springframework.stereotype.Controller class *

-keep @org.springframework.web.bind.annotation.RestController class *

-keep @jakarta.persistence.Entity class *

-keepattributes *Annotation*,Signature,InnerClasses

混淆解决的是“看不懂”,不是“拿不到”。真正不想直接暴露的计费、授权、规则计算模块,我会单独编译,再把 class 字节加密后放进 Jar。

下面这段是构建阶段使用的加密器,示例使用 AES-GCM。这里不用 ECB,那种写法我看到基本会直接改掉。

public final class ClassSeal {

  public static byte[] encrypt(byte[] classBytes, SecretKey key) throws Exception {

      byte[] nonce = new byte[12];

      SecureRandom.getInstanceStrong().nextBytes(nonce);

      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

      cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, nonce));

      byte[] encrypted = cipher.doFinal(classBytes);

      ByteBuffer packet = ByteBuffer.allocate(nonce.length + encrypted.length);

      packet.put(nonce);

      packet.put(encrypted);

      return packet.array();

  }

  private ClassSeal() {

  }

}

加密后的文件不要再保留.class后缀,可以放到:

BOOT-INF/classes/protected/pricing.bin

运行时由自定义类加载器读取并解密:

public final class ProtectedClassLoader extends SecureClassLoader {

  private final SecretKey key;

  public ProtectedClassLoader(ClassLoader parent, SecretKey key) {

      super(parent);

      this.key = key;

  }

  public Class<?> loadProtected(String className, byte[] packet) throws Exception {

      ByteBuffer buffer = ByteBuffer.wrap(packet);

      byte[] nonce = new byte[12];

      buffer.get(nonce);

      byte[] encrypted = new byte[buffer.remaining()];

      buffer.get(encrypted);

      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");

      cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, nonce));

      byte[] classBytes = cipher.doFinal(encrypted);

      return defineClass(className, classBytes, 0, classBytes.length);

  }

}

这里有个坑:不要把 AES 密钥写成 Java 常量。

private static final String KEY = "1234567890abcdef";

这么写等于没加密。反编译的人先搜SecretKeySpec、Cipher.getInstance,很快就能顺着代码把密钥翻出来。

密钥至少应该由启动环境注入:

java -Dapp.seal.key="${APP_SEAL_KEY}" -jar billing-service.jar

代码里只负责读取,不提供默认值:

String rawKey = System.getProperty("app.seal.key");

if (rawKey == null || rawKey.isBlank()) {

  throw new IllegalStateException("缺少运行密钥,拒绝启动");

}

更严格一点,可以让程序启动后拿机器指纹和许可证去授权服务换取临时密钥。这样客户复制一个 Jar 到另一台机器,程序也不能直接跑。

但话得说透:自定义类加载器解密后的字节码仍可能被 Java Agent、调试器或者内存转储拿走。把整个系统都塞进加密加载器,维护成本也很高,Spring 的扫描、代理、序列化随时可能出问题。

我实际更倾向于只保护少量核心模块。普通 CRUD 做混淆,数据库密码和令牌走配置中心,关键算法做加密加载,再补一层许可证校验。别把时间花在加密几十个 Controller 上,那些代码既不值钱,也最容易把启动流程折腾坏。

Jar 加密不是一堵墙,更像几道麻烦的门。门越多,对方拆代码的成本越高。做到这一步,通常已经够用了。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/OwvGbVXGm-V1Oc3qmftc1VMA0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券