Java代码保护方法之三:自定义类加载器

老丁谈职场 2025-02-02 10:52:04

大家好,我是小丁,一名小小程序员。

Java程序员应该都知道类加载器,双亲委托加载。

idea运行:

public static void main(String[] args) {System.out.println(OrderService.class.getClassLoader());}

运行这行代码,输出:

sun.misc.Launcher$AppClassLoader@18b4aac2

所以,如果使用idea运行程序,使用的加载器是AppClassLoader。

命令行运行:

java AppStart

输出:

jdk.internal.loader.ClassLoaders$AppClassLoader@18ff02e4

打成可执行jar包运行:

######maven配置<plugin><groupId>org.apache.maven.pluginsgroupId><artifactId>maven-assembly-pluginartifactId><version>3.3.0version><configuration><archive><manifest><mainClass>com.dxc.project1.AppStartmainClass>manifest>archive><descriptorRefs><descriptorRef>jar-with-dependenciesdescriptorRef>descriptorRefs>configuration><executions><execution><id>make-assemblyid><phase>packagephase><goals><goal>singlegoal>goals>execution>executions>plugin>

执行:

java -jar project.jar

输出:

jdk.internal.loader.ClassLoaders$AppClassLoader@18ff02e4

自定义类加载器

非springboot项目

新建自定义类加载器CustomClassLoader

package com.dxc.project1.service;import java.io.*;

public CustomClassLoader extends ClassLoader {    // 指定类文件的路径    private StringPath;

public CustomClassLoader(StringPath) {

this.classPath =Path;    }

@Override    public Class findClass(String name) throws ClassNotFoundException {

byte[]Data = null;

try {            // 将包名转换为路径,例如 "com.example.MyClass" 转换为 "com/example/MyClass.class"            String path =Path + "/" + name.replace('.', '/') + ".class";            InputStream inputStream = new FileInputStream(path);            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();

int len;

byte[] buffer = new byte[1024];

while ((len = inputStream.read(buffer)) != -1) {                byteStream.write(buffer, 0, len);            }            classData = byteStream.toByteArray();            inputStream.close();        } catch (IOException e) {            e.printStackTrace();

throw new ClassNotFoundException("Class " + name + " not found.");        }

if (classData == null) {

throw new ClassNotFoundException("Class " + name + " not found.");        } else {

return defineClass(name,Data, 0,Data.length);        }    }}

public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {

StringPath = "/Users/dxc/Desktop/project/ProGuardTest/project2/target/classes"; // 替换为你的类文件路径    CustomClassLoader customClassLoader = new CustomClassLoader(classPath);    try {        // 加载类并创建实例        Class clazz = customClassLoader.loadClass("com.dxc.project2.Student");

Object instance = clazz.getDeclaredConstructor().newInstance();        System.out.println(instance.getClass().getClassLoader());    } catch (Exception e) {        e.printStackTrace();    }}

输出:

com.dxc.project1.service.CustomClassLoader@5305068a

⚠️注意:

com.dxc.project2.Student不是classpath所在路径的类,不然即使你使用自定义类加载器,最后还是会使用AppClassLoader。

springboot项目

我们新建个springboot项目,看看它的默认的加载器。

####maven配置<plugin><groupId>org.springframework.bootgroupId><artifactId>spring-boot-maven-pluginartifactId><version>2.3.3.RELEASEversion><configuration><mainClass>com.dxc.project1.AppStartmainClass><fork>truefork>configuration><executions><execution><goals><goal>repackagegoal>goals>execution>executions>plugin>

@SpringBootApplication@ComponentScan(basePackages = {"com.dxc"})public AppStart {public static void main(String[] args) {SpringApplication.run(AppStart.class, args);System.out.println(OrderService.class.getClassLoader());}}

idea运行输出:

sun.misc.Launcher$AppClassLoader@18b4aac2

jar包运行:java -jar project.jar

org.springframework.boot.loader.LaunchedURLClassLoader@51521cc1

所以idea运行,用到的加载器是AppClassLoader,但是打成可执行jar包运行,使用的加载器是LaunchedURLClassLoader,这个类加载器会打包到可执行jar包里。

对于springboot项目,如果我们要使用自定义类加载器方案来做代码的保护,则需要先对打包好的jar包进行加密,一般的逻辑是:

1. 解析jar包中的文件,如果是.class文件,而且是需要加密的,则对class文件进行加密。

2. 在自定义类加载器中,先读取文件的字节码,判断前面四个字节是不是Java的魔鬼数字(0xCAFEBABE),如果不是魔数,则这个文件是加密后的,则进行解密;是魔数,则不需要解密。

在网上都没找到开箱即用的适合于springboot项目的类加载器,因为如果你要通过自定义类加载器来解密class文件,你可能要按照LaunchedURLClassLoader重写新的一套类加载器,然后设置上下文加载器,但是这个改动量较大。

@SpringBootApplication@ComponentScan(basePackages = {"com.dxc"})public AppStart {

public static void main(String[] args) {

Thread.currentThread().setContextClassLoader(new CustomClassLoader());        SpringApplication.run(AppStart.class, args);    }}

所以,对于springboot工程,需要通过自定义类加载器的方式来进行代码保护,我不太建议完全按照LaunchedURLClassLoader的方式来重写,因为需要修改的东西太多了,感兴趣的可以参考文章:

https://www.cnblogs.com/Chary/p/18277547

我推荐的方法:改写LaunchedURLClassLoader源码,重写defineClass。

先看下spring-boot-maven-plugin插件的依赖:

查看本地库的org.springframework.boot » spring-boot-loader-tools的内容:

参考文档:https://gitee.com/liu1204/plugin-gradle-spring-boot/blob/master/README.md

所以,spring-boot-maven-plugin插件会将这个jar包内loader目录下的spring-boot-loader.jar解压到可执行jar的根目录下,来作为springboot的启动和加载程序。

所以,如果要改写LaunchedURLClassLoader,则下载spring-boot-loader源码,改写源码后打包成jar,替换掉spring-boot-loader-tools中的spring-boot-loader.jar即可(⚠️注意版本对应)。

源码地址:

https://gitee.com/liu1204/plugin-gradle-spring-boot.git

代码修改:

通过LaunchedURLClassLoader源码,最终会调用的是:

这两个defineClass最终调用的是ClassLoader的

我们只要在LaunchedURLClassLoader中重写这个defineClass。但是因为这个函数是final,无法继承,这个方式就无法行得通。

建议的方式:

LaunchedURLClassLoader重写findClass,代码逻辑可以拷贝URLClassLoader的findClass,在获取字节码代码后:

byte[] b = res.getBytes();

增加判断和解密程序:

private static final int MAGIC_NUMBER = 0xCAFEBABE;boolean isMagicNumber(byte[] buffer) {// 检查缓冲区长度是否至少为4个字节if (buffer == null || buffer.length < 4) {return false;}// 将前四个字节转换为一个整数(大端字节序)int magicNumberFromBuffer = ((buffer[0] & 0xFF) << 24) |((buffer[1] & 0xFF) << 16) |((buffer[2] & 0xFF) << 8) |(buffer[3] & 0xFF);// 比较转换后的整数和特定的“魔鬼数字”return magicNumberFromBuffer == MAGIC_NUMBER;}

byte[] bytes = res.getBytes();//增加代码进行解密if (isMagicNumber(bytes)){//解密bytes = decrypt(bytes);}

...

所以,对于springboot程序,通过自定义加载器方式去做加解密,来保护代码,是一项吃力不讨好的事情,而且这块代码会直接暴露在jar包类,没有进行加密。

只要稍微资深的程序员,很容易逆向工程,得到解密后的class。此方案可以作为学习用,实际项目中用处不大。

END

这个方案虽然在表面上看起来颇为简单,但当我们深入探究其内部机制时,会发现其实它并不简单。

尽管如此,这个方案却为我们提供了一个宝贵的契机,使我们能够深入了解类加载器的核心原理,并学习如何根据实际需求去自定义类加载器。

尽管在实际项目中,我们可能并不会直接采用这个方案,但它所蕴含的知识点和技能,无疑对我们的编程能力和对Java深层次机制的理解有着极大的提升作用。

0 阅读:1
老丁谈职场

老丁谈职场

10年工作经历 一起聊聊职场那些事