目的

为什么需要自定义类加载器,其需求是什么?或者说其应用场景是什么?

  • 安全性:Java 代码能够被轻易的反编译,在足够优秀的 IDE 面前,class 文件等价于 java 文件,为了保证代码的安全性,可以将编译后的代码使用约定好的算法进行加密,这样的 class 文件时无法使用 Java 自有的类加载器读取的,此时就需要通过自定义加载器,在读取类时先解密后加载;
  • 资源隔离:对于同一 jvm 来说,不同类加载器实例化的同名类对象,实际上是不相等,每一个类加载器都相当于一个独立的容器;
  • 解决 jar 包冲突:基于上一点,同一个类名可以同时存在每一个类加载器中;

读取加密 class 文件

原理

自定义类加载器需要继承 ClassLoader 或其增强类 URLClassLoader,其中存在 findClass 方法,用于读取 class 文件,并通过搜索类的限定名对其进行加载,最终返回一个 Class 对象。
因此在读取 class 文件时,可以对其二进制字节进行解密,将其还原成原始的 class 文件,然后挂载到 jvm 中。

局限

通常情况下,只会使用 java 代码来实现自定义类加载器,不能加载其自身,即类加载器是可以作为反编译的切入点;同时,jvm 加载的类字节存在内存中,也是可以被访问的,狠一点的方法是通过其他语言来实现类加载器,但也不能完全避免反编译。

注意事项

  • 单例模式:自定义类加载器在使用时,会加载指定类,每次实例化类加载器,每个对象之间同样是资源隔离的,因此,频繁调用会使得 jvm 中挂载的类越来越多,最终导致内存溢出,因此需要将自定义类加载器设计成单例模式,spring 项目中,注册成 bean 即可被 spring 托管;

代码实现

类加载器

  • 继承自  URLClassLoader,特点是可以指定外部 jar 或 class 文件;
  • 主要重写  findClass 方法,用于读取 class 文件二级制字节并加载类;
  • 需要在读取字节后对其解密;
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
public class SimpleClassLoader extends URLClassLoader {

public SimpleClassLoader(URL... urls) {
super(urls);
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
for (URL url: this.getURLs()) {
byte[] bytes = decrypt(url) ;
// 将二进制字节转换成 java.lang.Class
return this.defineClass(name, bytes, 0, bytes.length);
}
return super.findClass(name);
}
private byte[] decrypt(URL url) {
try {
return SimpleEncryptUtils.decrypt(url.openStream());
} catch (IOException e) {
System.out.println("【类文件解码】jar 或 class 不存在");
return null;
}
}
public static void main(String[] args) {
testPeople();
}

private static void testPeople() {
// String target = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-origin.class";
String target = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-encrypt-1581006788342.class";
test(target, "com.example.demo.bean.People");
}
}

异或加密

  • 特点是简单,便于测试;
  • 核心方法是  byte[] encrypt(byte[] bytes, String key),其他方法都是基于此方法的重载,便于使用;
  • main  方法中对  People.class  文件使用异或加密,并导出得到加密后的文件;
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class SimpleEncryptUtils {

public static final String DEFAULT_KEY = "daiwenzh5♪(^∀^●)ノ";

public static byte[] encrypt(byte[] bytes, String key) {
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < bytes.length; i++) {
for (byte keyByte : keyBytes) {
bytes[i] = (byte) (bytes[i] ^ keyByte);
}
}
return bytes;
}

public static byte[] encrypt(byte[] bytes) {
return encrypt(bytes, DEFAULT_KEY);
}

public static byte[] encrypt(InputStream inputStream) {
return encrypt(inputStream, DEFAULT_KEY);
}

public static byte[] encrypt(InputStream inputStream, String key) {

try {
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);
byte[] bytes = new byte[bufferedInputStream.available()];
bufferedInputStream.read(bytes);
return encrypt(bytes, key);
} catch (IOException e) {
System.out.println(e.getMessage());
}
return new byte[0];
}

public static byte[] decrypt(byte[] bytes, String key) {
// 因为是取反,所以解密过程和加密过程一致
return encrypt(bytes, key);
}

public static byte[] decrypt(byte[] bytes) {
// 因为是取反,所以解密过程和加密过程一致
return decrypt(bytes, DEFAULT_KEY);
}

public static byte[] decrypt(InputStream inputStream) {
// 因为是取反,所以解密过程和加密过程一致
return decrypt(inputStream, DEFAULT_KEY);
}

public static byte[] decrypt(InputStream inputStream, String key) {
// 因为是取反,所以解密过程和加密过程一致
return encrypt(inputStream, key);
}

public static void encryptFile(String src, String dist) {
try {
Path path = Paths.get(dist);
if (!Files.exists(path)) {
Files.createFile(path);
}
Files.write(path, Objects.requireNonNull(encrypt(Files.newInputStream(Paths.get(src)))));
} catch (IOException e) {
e.printStackTrace();
}
}

public static void decryptFile(String src, String dist) {
encryptFile(src, dist);
}

private static void testDecryptFile() {
long time = System.currentTimeMillis();
// 加密文件
// String src = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-origin.class";
String src = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-encrypt-1581006788342.class";
// 解密路径
String dist = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-decrypt-" + time + ".class";
System.out.printf("文件名:People-decrypt-%d.class%n", time);
decryptFile(src, dist);
}

private static void testEncryptFile() {
long time = System.currentTimeMillis();
// 源文件
String src = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-origin.class";
// 加密路径
String dist = "C:\\Users\\daiwenzh5\\Desktop\\test\\People-encrypt-" + time + ".class";
System.out.printf("文件名:People-encrypt-%d.class%n", time);
encryptFile(src, dist);
}

private static void testString(String content) {
byte[] encrypt = encrypt(content.getBytes(StandardCharsets.UTF_8));
System.out.println(new String(encrypt, StandardCharsets.UTF_8));
System.out.println(new String(decrypt(encrypt), StandardCharsets.UTF_8));
}

public static void main(String[] args) {
// testString("hello world");
testEncryptFile();
// testDecryptFile();
}
}

class 文件

执行结果

  • 加密 class 文件被成功读取,并通过反射正确输出对象类名;
  • 如期地打印其使用的类加载器;
1
2
People
com.example.demo.configs.SimpleClassLoader@1218025c

评论