spring项目的代码混淆(proguard)

前不久被要求对java web的war包做代码混淆,我使用proguard6.0.3完成的。后面也许还会用到,因此记录下过程和配置方法,demo代码按照图1结构进行组织。仅对混淆功能进行说明,其他功能如压缩、优化不考虑。

混淆需求

混淆代码的需求如下:

  1. 混淆包名、类名、成员属性、方法名等。
  2. 不混淆涉及到dao命名空间配置、aop切点的类名或方法名。
  3. 不混淆注解。
  4. 不混淆proguard默认配置的类名或方法名。

本Demo中按照需求表现为:

  1. 混淆controllerservice中的包名、类名、成员属性、方法名。
  2. 假设aop切面涉及到controller包名,因此不混淆controller包名;假设aop切面涉及到service.findOne()方法,因此不混淆service类的findOne()方法;dao中所有类涉及到mybatis命名空间,因此不混淆dao中所有类。
  3. 不能混淆注解,如@RestController@Service等,否则spring无法正确解析。
  4. proguard默认配置了不混淆enumnative方法等,保留这些默认配置。

图1 Demo代码包结构

PROGUARD配置实践

proguard的配置可以先使用GUI版程序进行配置,然后将配置复制出来保存为.pro文件,再使用命令行执行混淆操作(命令行执行混淆比GUI更快)。

图2~图4为GUI界面的混淆配置:

图2 PROGUARD混淆界面

图2中显示了混淆的基本配置:

  1. Obfuscates表示开启混淆。
  2. Print mapping表示混淆的映射保存到的文件,一定要保存,否则后期排查无法找到混淆前的名字。
  3. Keep package names表示不混淆的包名。
  4. Keep attributes表示不混淆的属性,其中包含Annotation
  5. Native method names表示不混淆native方法
  6. Keep additional class names and class member names表示不混淆类名或成员名,用于配置具体的不混淆的类、属性或方法,见图3、图4。

图3为类名的混淆配置,即配置不混淆的类(keep表示保留,即不混淆)。Class栏填写表示不混淆的类,现需求为只对controllerservice进行混淆,也就是除了这2个包之外所有的包都不混淆,所以是keep!controller!service包,那么配置将为图3中所示。

图3 类名混淆配置

根据测试,发现proguard的设定为:如果只设置Class栏,而不设置Class member栏,那么只会保留其他包和类的包名和类名,而成员属性和方法依然会被混淆,因此必须指定所有的属性和方法名,即图3中所示(也可为*)。

图4为方法名(或成员属性)的混淆配置,即配置不混淆的方法名,按图配置即可,Keep栏需要选择Keep class members only

图4 方法名混淆配置

混淆的配置暂时只用到上述配置,其余配置含义可以通过查阅proguard文档获知。

另外,还有一些额外的附加配置,比如打印参数,JDK版本以及jar包问题,图5为额外配置。

图5 额外配置

  1. Preverify勾选表示开启预检查,建议勾选,否则可能混淆出来的jar或class无法使用。
  2. Target选择对应版本,选错了好像也没有发现有什么异样。
  3. Note potential mistakes in the configuration去掉勾选表示不打印NOTE信息,建议去掉勾选,否则控制台会打印很多没必要查看的信息,并且会降低混淆执行速度。
  4. Warn about possibly erroneous input勾选表示打印WARN信息,建议勾选,如果有错误可以查看。
  5. Ignore warnings about possibly erroneous input勾选表示忽略warn信息,建议勾选,否则会因为warn而无法混淆,很多时候warn只作参考,并不会对混淆结果造成影响。
  6. Skip non-public library class members默认会勾选,据提示信息可以得知可以执行更快速,但如果有需求也可以去掉勾选。
  7. Keep directories勾选表示混淆后的jar包保持原有的目录,如果混淆了包名,那么这些包名原始的目录或者其中的目录都将是空文件夹,建议勾选,否则有可能运行混淆的代码找不到jar包中的class。

配置完毕后,到proguard左侧的Process里,执行下方的View configuration可以得到配置脚本,脚本见文末案列.pro配置文件及注释

混淆WAR包实践

从图1中可以看到,commonrepositoryservice模块是单独的模块,分别包含各自模块或MVC架构层的代码,web模块是webapp所在的模块,因此当项目打war包后会出现图6所示的包结构,前3者的包会以jar包的形式出现在lib目录中,而web模块中的controller代码将在war包的classes中。

图6 打war包后的包结构

此时如果按照这样的包结构直接将war包执行混淆,那么混淆后的war包会出现web模块的controller代码无法混淆的情况,并且如果没有设置指定keepdirectories,那么controller等代码还会被删除掉。

出现上述问题的原因是proguard只支持混淆jar包和存放在包名层级目录的父目录下的class文件。jar包很好理解,关键是class文件的混淆。假设class为p1.p2.C1,那么对应的包名层级目录为p1/p2/C1.class,此时如果要混淆C1.class,那么需要上述包名层级目录有父目录,假设为classes,即目录层级为classes/p1/p2/C1.class,然后proguard配置路径为classes才可行。

备注:如果proguard配置路径为class文件,即C1.class则会得到warn提示Warning: class [C1.class] unexpectedly contains class [p1.p2.C1]而导致无法混淆;如果配置路径不是classess而是p1,那么将得到warn提示Warning: class [p2/C1.class] unexpectedly contains class [p1.p2.C1]。看到这里应该能明白为什么要配置为父目录classes了吧。

再回过来看看图6,war包根目录下的文件夹是WEB-INF,然后才是class层级目录的父目录classes,根据上述描述,proguard只支持混淆存放在包名层级目录的父目录下的class文件,因此如果直接对war执行混淆,controller的代码会多出一层目录,导致无法混淆。

如果要执行混淆,办法有2个:

1、将web包下的所有class代码移到单独的模块,与common等这种模块类似

2、war包拆开后对jar包和class文件分别混淆,再合并打成war包

方法1肯定更方便,但是由于项目已经按照图6的结构完成了,如果贸然改变包结构可能会有风险,而且其他项目也是按照这种结构架设的,如果都去更改模块结构会特别麻烦。因此考虑采用方法2,这种方式虽然看起来麻烦,但可以编写脚本来完成拆包和打包任务,最重要的是其他项目也是可以复用该脚本的。

拆分war包需要将war包解压,然后对jar包和WEB-INF/classes下的class文件分别混淆,再将混淆后的文件合并回解压文件夹,最后再重新打成war包即可。具体方法不赘述,仅列出war包的解压、打包方法、混淆命令行语句。需要注意的是合并混淆文件之前要先删除WEB-INF/classes下的class文件,因为混淆后包名会改变,不会覆盖原先的文件,而jar包混淆后文件名不变,是可以覆盖原先的jar包的。

1
2
3
4
5
6
7
8
# 解压,只能解压到当前目录
jar -xf proj.war

# 打包,将proj下面的所有文件打包到proj-obfuscate.war包中
jar -cf proj-obfuscate.war -C proj/ .

# proguard按照config.pro执行
java -jar proguard.jar @config.pro

执行完成,可以得到混淆后的war包,包结构如图7所示。

图7 混淆后的war包结构

案例代码

1. .pro配置文件及注释

1
2
3
4
5
6
7
# 输入输出,分别配置classes下的class文件和jar包混淆的输入输出
# 括号中为过滤的文件,即不查询的文件,如果混淆过程中报错文件找不到,可以将该文件过滤掉
-injars temp-orgin\WEB-INF\classes(!org/aspectj/org/eclipse/jdt/internal/compiler/parser/UpdateParserFiles.class)
-outjars temp-obfuscated\WEB-INF\classes

-injars temp-orgin\WEB-INF\lib\service-6.0.3.jar(!org/aspectj/org/eclipse/jdt/internal/compiler/parser/UpdateParserFiles.class)
-outjars temp-obfuscated\WEB-INF\lib\service-6.0.3.jar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 混淆配置
-target 1.8
-libraryjars C:\Program Files\Java\jdk1.8.0_40\jre\lib\rt.jar
-dontnote
-ignorewarnings
-dontshrink
-dontoptimize
-keepdirectories
-printmapping 'mapping.txt'

# keep配置
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,LocalVariable*Table,*Annotation*,Synthetic,EnclosingMethod

# controller包名不混淆,否则aop无法切入
-keeppackagenames online.dinghuiye.controller

# Obfuscate controller and service
-keep class !online.dinghuiye.controller.**,!online.dinghuiye.service.** {
<fields>;
<methods>;
}

# 不混淆指定方法名的方法
-keepclassmembers class * {
*** findOne(...);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 保留的proguard的默认配置

# Also keep - Enumerations. Keep the special static methods that are required in
# enumeration classes.
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

# Also keep - Database drivers. Keep all implementations of java.sql.Driver.
-keep class * extends java.sql.Driver

# Also keep - Swing UI L&F. Keep all extensions of javax.swing.plaf.ComponentUI,
# along with the special 'createUI' method.
-keep class * extends javax.swing.plaf.ComponentUI {
public static javax.swing.plaf.ComponentUI createUI(javax.swing.JComponent);
}

# Keep - Native method names. Keep all native class/method names.
-keepclasseswithmembers,includedescriptorclasses class * {
native <methods>;
}

2. 混淆前后代码对比

  • SomeUtil没有混淆

  • SomeDao没有混淆

  • SomeService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 混淆前
package online.dinghuiye.service;

import java.util.List;

public interface SomeService
{
Object findOne();

List<Object> findList();
}

// 混淆后
package a.a.a;

import java.util.List;

public abstract interface a
{
public abstract Object findOne();

public abstract List<Object> a();
}
  • SomeServiceImpl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 混淆前
package online.dinghuiye.service.impl;

import online.dinghuiye.repository.dao.SomeDao;
import online.dinghuiye.service.SomeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class SomeServiceImpl implements SomeService
{
@Autowired
private SomeDao dao;

@Override
public Object findOne() {
List<Object> list = dao.findList();
if (list.size() > 0) {
return list.get(0);
}
return null;
}

@Override
public List<Object> findList() {
return dao.findList();
}
}

// 混淆后
package a.a.a.a;

import java.util.List;
import online.dinghuiye.repository.dao.SomeDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class a implements a.a.a.a
{
@Autowired
private SomeDao a;

public Object findOne()
{
List<Object> list = this.a.findList();
if (list.size() > 0) {
return list.get(0);
}
return null;
}

public List<Object> a()
{
return this.a.findList();
}
}
  • SomeController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 混淆前
package online.dinghuiye.controller;

import online.dinghuiye.service.SomeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/")
public class SomeController
{
@Autowired
private SomeService service;

@RequestMapping("list")
public Object findList() {
return service.findList();
}
}

// 混淆后
package online.dinghuiye.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping({"/"})
public class a
{
@Autowired
private a.a.a.a a;

@RequestMapping({"list"})
public Object a()
{
return this.a.a();
}
}

3. mapping.txt

1
2
3
4
5
6
7
8
9
10
11
12
online.dinghuiye.controller.SomeController -> online.dinghuiye.controller.a:
online.dinghuiye.service.SomeService service -> a
13:13:void <init>() -> <init>
20:20:java.lang.Object findList() -> a
online.dinghuiye.service.SomeService -> a.a.a.a:
java.lang.Object findOne() -> findOne
java.util.List findList() -> a
online.dinghuiye.service.impl.SomeServiceImpl -> a.a.a.a.a:
online.dinghuiye.repository.dao.SomeDao dao -> a
14:14:void <init>() -> <init>
21:25:java.lang.Object findOne() -> findOne
30:30:java.util.List findList() -> a

参考文档

封面图来源:https://www.cnblogs.com/lmq3321/p/10320671.html