专业的JAVA编程教程与资源

网站首页 > java教程 正文

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

temp10 2025-02-06 16:26:37 java教程 8 ℃ 0 评论

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

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

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配置


org.apache.maven.pluginsgroupId>

maven-assembly-plugin
3.3.0



com.dxc.project1.AppStart



jar-with-dependencies






make-assembly
package

single




执行:

java -jar project.jar

输出:

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


自定义类加载器

非springboot项目

新建自定义类加载器CustomClassLoader

package com.dxc.project1.service;

import java.io.*;

public class CustomClassLoader extends ClassLoader {

    // 指定类文件的路径
    private String classPath;

    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        byte[] classData = null;
        try {
            // 将包名转换为路径,例如 "com.example.MyClass" 转换为 "com/example/MyClass.class"
            String path = classPath + "/" + 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, classData, 0, classData.length);
        }
    }
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
    String classPath = "/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配置

org.springframework.boot
spring-boot-maven-plugin
2.3.3.RELEASE

com.dxc.project1.AppStart
true




repackage




@SpringBootApplication
@ComponentScan(basePackages = {"com.dxc"})
public class 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 class 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深层次机制的理解有着极大的提升作用。

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表