使用
proguard
混淆代码
只能增加阅读和理解的难度
, 并不能百分百保证代码安全。常用的应用场景是项目需要部署到客户机器上,一定程度上防止代码泄露。
proguard 简介
ProGuard 是一个混淆代码的开源项目,它的主要作用是混淆代码,ProGuard 包括以下 4 个功能:
-
压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)
-
优化(Optimize):对字节码进行优化,移除无用的指令
-
混淆(Obfuscate):使用 a,b,c,d 这样简短而无意义的名称,对类、字段和方法进行重命名
-
预检(Preveirfy):在 Java 平台上对处理后的代码进行预检,确保加载的 class 文件是可执行的
实战示例
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
混淆效果
用于进一步查看打包后的 jar 文件是否符合要求,同时还可以辅助查找 jar 文件运行失败的原因
常见的反编译工具使用
jd-gui
。下载地址:
http://java-decompiler.github.io/
。
直接通过
jd-gui
窗口打开编译打包后的 jar 文件即可。
参考文档
ProGuard manual
Code obfuscation for Spring Boot applications using the ProGuard plugin proguard-maven-plugin
springboot proguard 代码混淆