使用 ​​proguard​​ 混淆代码只能增加阅读和理解的难度, 并不能百分百保证代码安全。常用的应用场景是项目需要部署到客户机器上,一定程度上防止代码泄露。

proguard 简介

ProGuard 是一个混淆代码的开源项目,它的主要作用是混淆代码,ProGuard 包括以下 4 个功能:

  • 压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)
  • 优化(Optimize):对字节码进行优化,移除无用的指令
  • 混淆(Obfuscate):使用 a,b,c,d 这样简短而无意义的名称,对类、字段和方法进行重命名
  • 预检(Preveirfy):在 Java 平台上对处理后的代码进行预检,确保加载的 class 文件是可执行的

springboot+proguard+maven 实现代码混淆 看这一篇就够了_spring boot

实战示例

proguard 是广为使用的工具之一,可是用他的客户端方式来混淆 springboot 项目的时候最后总得不到可执行的 jar。后来发现了 ​​proguard-maven-plugin​​​ 这个插件,所有 proguard 的指令都可以在 pom 中进行定义实现,本文基于 ​​springboot2.x + maven + proguard​​ 架构进行代码混淆。

源码:

​https://github.com/hacfins/spring-boot-2-api​

修改 pom 文件

定义 ​​proguard-maven-plugin​​​ 插件且插件位于 ​​spring-boot-maven-plugin​​ 插件的前面

  • proguardInclude
    表示使用外部扩展的配置文件 ​​proguard.cfg​​,和 pom.xml 同目录
  • keepparameternames
    此选项将保留所有原始方法参数,controller 如果函数的参数也混淆(如混淆为a、b、c等)会导致传参映射不上

详细配置如下(由于有详细注释,不再一一说明):

<build>
<plugins>
<!--proguard混淆插件-->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>${proguard-maven-plugin.version}</version>
<executions>
<execution>
<!--打包的时候开始混淆-->
<phase>package</phase>
<goals>
<goal>proguard</goal>
</goals>
</execution>
</executions>

<configuration>
<proguardVersion>${proguard.version}</proguardVersion>
<injar>${project.build.finalName}.jar</injar>
<!--输出的jar-->
<outjar>${project.build.finalName}.jar</outjar>
<!--是否混淆-->
<obfuscate>true</obfuscate>
<proguardInclude>${basedir}/proguard.cfg</proguardInclude>

<options>
<!--默认开启,不做收缩(删除注释、未被引用代码)-->
<option>-dontshrink</option>
<!--默认是开启的,这里关闭字节码级别的优化-->
<option>-dontoptimize</option>
<!--对于类成员的命名的混淆采取唯一策略-->
<option>-useuniqueclassmembernames</option>
<!--混淆时不生成大小写混合的类名,默认是可以大小写混合-->
<option>-dontusemixedcaseclassnames </option>
<!--混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代-->
<option>-adaptclassstrings</option>

<!--对异常、注解信息在runtime予以保留,不然影响springboot启动-->
<option>-keepattributes
Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
</option>

<!--此选项将保存接口中的所有原始名称(不混淆)-->
<option>-keepnames interface ** { *; }</option>
<!--此选项将保存所有软件包中的所有原始接口文件(不进行混淆)-->
<!--<option>-keep interface * extends * { *; }</option>-->

<!--此选项将保留所有原始方法参数,controller如果参数也混淆会导致传参映射不上 -->
<option>-keepparameternames</option>

<!--保留枚举成员及方法-->
<option>-keepclassmembers enum * { *; }</option>

<!--不混淆所有类,保存原始定义的注释-->
<!--<option>-keepclassmembers class * {
@org.springframework.context.annotation.Bean *;
@org.springframework.beans.factory.annotation.Autowired *;
@org.springframework.beans.factory.annotation.Value *;
@org.springframework.stereotype.Service *;
@org.springframework.stereotype.Component *;
}
</option>-->

<!--忽略warn消息-->
<option>-ignorewarnings</option>
<!--忽略note消息-->
<option>-dontnote</option>
</options>
<!--java 11-->
<libs>
<lib>${java.home}/jmods/</lib>
</libs>
<!--java 8-->
<!-- <libs>
<lib>${java.home}/lib/rt.jar</lib>
<lib>${java.home}/lib/jsse.jar</lib>
</libs>-->
</configuration>
<dependencies>
<dependency>
<groupId>com.guardsquare</groupId>
<artifactId>proguard-base</artifactId>
<version>${proguard.version}</version>
</dependency>
</dependencies>
</plugin>

<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--jar可直接运行-->
<executable>true</executable>
</configuration>
</plugin>
</plugins>
</build>

proguard.cfg

作为 pom.xml 中的扩展配置,详细配置如下:

#所有类(包括接口)的方法参数不混淆(包括没被keep的),如果参数混淆了,mybatis的mapper参数绑定会出错(如#{id})
-keepattributes MethodParameters

#入口程序类不能混淆,混淆会导致springboot启动不了
-keep class com.langyastudio.edu.admin.Application {
public static void main(java.lang.String[]);
}

#mybatis的mapper/实体类不混淆,否则会导致xml配置的mapper找不到
-keep class com.langyastudio.edu.admin.dao.*
-keeppackagenames com.langyastudio.edu.admin.dao

#考虑到scanBasePackages,需要包名不被修改
-keeppackagenames com.langyastudio.edu
-keeppackagenames com.langyastudio.edu.admin.common

#一些配置类比如datasource,aopconfig如果混淆会导致各种启动报错
# 比如用@Pointcut("execution(public * com.langyastudio.edu.*.controller..*.*(..))")
# 指定webLog方法对应的@Pointcut作为切入点,所以包的名字不能修改
-keeppackagenames com.langyastudio.edu.*.controller.**
-keep class com.langyastudio.edu.admin.config.*

#保留Serializable序列化的类不被混淆
#例如传入/输出的Bean属性
-keepclassmembers class * implements java.io.Serializable {*;}

#保留空的构造函数
#-keepclassmembers class com.hacfin.* {
# public <init>(...);
#}

混淆配置要点

  • 建议逐个 java 包定义混淆规则,这样思路更清晰
  • repository(dao)层需要保存包名和类名,因为 Mybatis 的 xml 文件中引用了dao 层的接口
  • controller 层注意在使用 @PathVariable、@RequestParam 时需要显式声明参数名
  • dao 层用于映射数据库表的类和 controller 层映射前台参数的类,都需要保留类成员
  • 修改 spring 的 bean 命名策略,改成按类的全限定名来命名
  • 等等

入口程序类

  • 入口程序类不能混淆,混淆会导致 springboot 启动不了,增加如下配置:
-keep class com.langyastudio.edu.admin.Application {
public static void main(java.lang.String[]);
}

bean 名称冲突

  • 默认混淆后的类名为 xx.a.b、xx.c.a,直接使用混淆后的类名作为 bean 会引发重名异常,所以需要修改 BeanName 生成策略。

不能重写 generateBeanName 方法,因为有些 Bean 会自定义 BeanName,所以这些情况还需要走原来的逻辑。

public class Application
{
public static void main(String[] args)
{
new SpringApplicationBuilder(Application.class)
.beanNameGenerator(new UniqueNameGenerator())
.run(args);
}

/**
* 由于需要混淆代码,混淆后类都是A B C,spring 默认是把A B C当成BeanName,BeanName又不能重复导致报错
* 所以需要重新定义BeanName生成策略
*/
@Component("UniqueNameGenerator")
public static class UniqueNameGenerator extends AnnotationBeanNameGenerator
{
/**
* 重写buildDefaultBeanName
* 其他情况(如自定义BeanName)还是按原来的生成策略,只修改默认(非其他情况)生成的BeanName带上包名
*/
@Override
public @NotNull String buildDefaultBeanName(BeanDefinition definition)
{
//全限定类名
return Objects.requireNonNull(definition.getBeanClassName());
}
}
}

包名保留

  • 考虑到 ​​scanBasePackages​​ 等特殊的注解配置,需要包名不被修改,配置如下:
-keeppackagenames com.langyastudio.edu
  • ​scanBasePackages​​ 样例:

如果 ​​com.langyastudio.edu​​​ 名称被混淆,将导致 ​​scanBasePackages​​ 失效

@SpringBootApplication(scanBasePackages = {"com.langyastudio.edu.*"})
public class Application
{
public static void main(String[] args)
{
xxxxx
}
}

配置类

  • 一些配置类比如 datasource、aop、config 等如果混淆会导致各种启动报错
    比如用 ​​@Pointcut("execution(public * com.langyastudio.edu.*.controller..*.*(..))")​​ 指定 ​​webLog​​ 方法对应的 @Pointcut 作为切入点。所以包的名字与函数名称不能修改
-keeppackagenames com.langyastudio.edu.*.controller.**
-keep class com.langyastudio.edu.admin.config.*
  • 配置类样例:
public class WebLogAspect
{
/**
* 包及其子包下所有类中的所有方法都应用切面里的通知
*/
@Pointcut("execution(public * com.langyastudio.edu.*.controller..*.*(..))")
public void webLog()
{
}

@Before("webLog()")
public void doBefore(JoinPoint joinPoint) throws Throwable
{
}
}

dao 层

  • mybatis 的 mapper/实体类 不能混淆,否则会导致 xml 配置的 mapper 找不到
-keep class com.langyastudio.edu.admin.dao.*
-keeppackagenames com.langyastudio.edu.admin.dao
#接口类保留
xxxx

bean 属性保留

  • controller 层映射前台参数的类、后端返回的 bean 属性类等,不能混淆类的成员属性(如变成 ​​string a;​​)
  • 修改方案为保留 ​​Serializable​​ 序列化的类成员不被混淆
    ​-keepclassmembers class * implements java.io.Serializable {*;}​
  • bean 样例:
    需要将原有的属性类增加 ​​Serializable​​ 的继承
@Data
@NoArgsConstructor
public class TokenVO implements Serializable
{
/**
* token
*/
private String token;

/**
* token 前缀
*/
private String tokenHead;
}

编译打包

打包成功一定要运行 编译后的 jar ,测试是否能否正常运行

mvnw clean package  -Dmaven.test.skip=true

springboot+proguard+maven 实现代码混淆 看这一篇就够了_安全_02

混淆效果

用于进一步查看打包后的 jar 文件是否符合要求,同时还可以辅助查找 jar 文件运行失败的原因

常见的反编译工具使用 ​​jd-gui​​​。下载地址:​​http://java-decompiler.github.io/​​。

直接通过 ​​jd-gui​​ 窗口打开编译打包后的 jar 文件即可。

springboot+proguard+maven 实现代码混淆 看这一篇就够了_jar_03

参考文档

​ProGuard manual​

​Code obfuscation for Spring Boot applications using the ProGuard plugin proguard-maven-plugin​

​​springboot proguard 代码混淆​​