自定义类加载器加载加密jar包,使用Reflections扫描自定义加载器加载的Class

2021/4/15 19:00:59

本文主要是介绍自定义类加载器加载加密jar包,使用Reflections扫描自定义加载器加载的Class,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

为什么要做这个工作:

    游戏私服是游戏人最讨厌的一件事,而游戏私服基本上都是内部人员把内部的自启服务器泄露出去,我们现在做的就是,内部发行的服务器版本是加密后的二进制文件,必须用给定的RSA秘钥才能解密二进制文件,然后 再使用自定义类加载器进行加载,在整个过程中都是流操作,不会生成class文件,就能防止内部发行的服务器被拷贝。这样并不能完全防止服务器泄露,如果有心人拿到秘钥,拿到加密后的class,自己写代码解密,也不能完全禁止,但是使用秘钥能在服务器删除秘钥,设置有效期,能在一定程度上加大服务器泄露的成本,本身安全就不是绝对的,能做的只是加大泄露的难度。

   开始工作之前一定要对类加载器的工作机制有深入的理解,启动类加载器,扩展类加载器,系统类加载器,另外还有上下文类加载器

   关于类加载器参考:https://blog.csdn.net/justloveyou_/article/details/72217806

   关于上下文类加载器参考:https://blog.csdn.net/justloveyou_/article/details/72231425

   启动完成自定义加载器,有时候我们想通过Reflections去扫描class,如下所示:

Reflections reflections = new Reflections(packagePath);

  如果不做一些特殊处理,自定义类加载器的加载的class是不会被扫描到的。

  接下来做三个工作:1定义类加载器  2定义启动类  3定义自己的容器扫描路径。

1. 定义自己的类加载器

   自定义类加载器中缓存的是使用AES加密后文件的二进制字节数据,在解密,加载类过程中不会生成class文件,防止jar包外泄。

package earth.pack.container;

import earth.support.RSAUtils;
import java.security.Key;
import java.util.HashMap;
import java.util.Set;

/**
 * @author zhangjiaqi
 * @desc: 容器的类加载器
 * @time 2021/4/9 11:18
 */
public class ContainerLoader extends ClassLoader {

    /**
     * 所有加密后的文件的二进制流
     */
    private HashMap<String, byte[]> allFiles = new HashMap<String, byte[]>();

    /* 密文key */
    private Key key = null;

    public ContainerLoader(ClassLoader parent, Key key, HashMap<String, byte[]> allFiles) {
        // 这里就算是不设置 也会把自动设置父类加载器为系统加载器
        super(parent);
        this.key = key;
        this.allFiles = allFiles;
    }

    /**
     * 重写findClass方法
     *
     * @param name 是我们这个类的全路径
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class log = null;
        // 获取该class文件字节码数组
        byte[] classData = getData(name);

        if (classData != null) {
            // 将class的字节码数组转换成Class类的实例
            log = defineClass(name, classData, 0, classData.length);
        }
        return log;
    }

    /**
     * 获取解密后的二进制流
     *
     * @param name
     * @return
     */
    public byte[] getData(String name) {

        byte[] bytes = null;
        if (allFiles.containsKey(name)) {
            byte[] encryptBytes = allFiles.get(name);
            try {
                bytes = RSAUtils.decryptByAES(encryptBytes, key);
            } catch (Exception e) {
                e.printStackTrace();
            }

        } else {
            System.out.println("error: " + name);
        }
        return bytes;

    }
}

2 启动类加载器

  我们这里是通过http从服务器取AES密文,当然整个过程都是通过RSA加密传输的。

package earth.pack.container;

import com.alibaba.fastjson.JSONObject;
import earth.config.ContainerConfig;
import earth.enums.EnvParamName;
import earth.pack.container.reflection.ContainerDir;
import earth.pack.container.reflection.ContainerFile;
import earth.pack.container.reflection.ContainerHandler;
import earth.pack.container.reflection.ContainerType;
import earth.support.HttpClient;
import earth.support.RSAUtils;
import earth.utils.FileUtils;
import org.reflections.vfs.Vfs;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.Key;
import java.util.*;

/**
 * @author zhangjiaqi
 * @desc: 加密容器类 全程流操作 不会中途生成文件
 * @time 2021/4/9 11:18
 */
public class ContainerApp {

    private static ContainerConfig config = null;
    public static final String USER_DIR = System.getProperty("user.dir");
    private static final String encryptPath = USER_DIR + File.separator + "encrypt.tar.gz";
    public static final String CONTAINER_FILE = "CONTAINER_FILE";
    public static final String CONTAINER_TYPE = "container";
    private static ContainerLoader loader = null;

    private ContainerApp() {
    }

    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("args size error !!!");
            return;
        }

        // 初始化配置
        initConf();

        // 生成对象为了调用父类加载器
        ContainerApp app = new ContainerApp();

        // 解压缩二进制流
        HashMap<String, byte[]> allFiles = unTarFile();

        // 网络取RSA数据
        String data = getRsa();

        // 分析密文key
        Key key = parseAES(data);

        loader = new ContainerLoader(app.getClass().getClassLoader(), key, allFiles);

        app.startContainer(allFiles, args[0], args[1]);

    }

    public static ContainerLoader getLoader(){
        return loader;
    }

    /**
     * 初始化配置
     */
    public static void initConf() {

        String cfgText = null;
        try {
            cfgText = FileUtils.getStringFromFile(System.getProperty(CONTAINER_FILE));
        } catch (Exception e) {
            e.printStackTrace();
        }
        config = JSONObject.parseObject(cfgText, ContainerConfig.class);
    }

    /**
     * 解压缩文件 转换为二进制流
     *
     * @return
     */
    public static HashMap<String, byte[]> unTarFile() {
        // 解压
        HashMap<String, byte[]> allFiles = null;
        try {
            File encryptFile = new File(encryptPath);
            if (!encryptFile.exists()) {
                System.out.println("encrpt file is null, path:" + encryptPath);
                System.exit(1);
            }

            allFiles = FileUtils.unTarGz(encryptFile);

        } catch (Exception e) {
            e.printStackTrace();
            System.exit(1);
        }
        return allFiles;
    }

    /**
     * 开始容器
     * @param allFiles 加密后的文件二进制流
     * @param mainClass 游戏服启动类
     * @param startMethod 游戏服启动方法
     */
    private void startContainer(HashMap<String, byte[]> allFiles, String mainClass, String startMethod) {
        try {

            // 解析后的 class二进制流
            List<Vfs.File> fileList = new ArrayList<>();
            for (String name : allFiles.keySet()) {
                byte[] bytes = loader.getData(name);
                Vfs.File file = new ContainerFile(name, name, new ByteArrayInputStream(bytes));
                fileList.add(file);
            }

            // 自定义容器一个加载类型,Reflections构造方法用
            ContainerType type = new ContainerType(new ContainerDir(fileList));
            Vfs.addDefaultURLTypes(type);

            Class<?> clazz = loader.loadClass(mainClass);
            Constructor constructor = clazz.getDeclaredConstructor();
            constructor.setAccessible(true);
            Method method = clazz.getDeclaredMethod(startMethod);
            method.setAccessible(true);
            method.invoke(constructor.newInstance());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 分析AES
     *
     * @param data
     * @return
     */
    public static Key parseAES(String data) {


        Key key = null;
        try {
            JSONObject resp = JSONObject.parseObject(data);
            String dataEncrypt = resp.getJSONObject("data").getString("data");
            String signS = resp.getJSONObject("data").getString("sign");
            String dataDecrrypt = RSAUtils.decrypt(dataEncrypt, config.getPrivate_key());
            boolean ok = RSAUtils.verify(dataEncrypt, signS, config.getPublic_key());
            if (!ok) {
                System.out.println("sign verify fail!!!");
                return null;
            }
            // 只用16位密文
            if (dataDecrrypt.length() > 16) {
                dataDecrrypt = dataDecrrypt.substring(0, 16);
            }
            key = new SecretKeySpec(dataDecrrypt.getBytes(), "AES");
            return key;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取RSA秘钥
     *
     * @return
     */
    public static String getRsa() {

        String cfgText = null;
        try {
            cfgText = FileUtils.getStringFromFile(System.getProperty(EnvParamName.CONFIG_FILE.name()));
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
        JSONObject startJSON = JSONObject.parseObject(cfgText);

        String gameName = startJSON.getString("game").toUpperCase();
        String serverId = startJSON.getString("id");
        String version = startJSON.getString("platform").toUpperCase();



        String rtn = null;
        try {
            String ptext = "gameName" + gameName + "serverId" + serverId + "version" + version;
            String sign = RSAUtils.sign(ptext, config.getPrivate_key());

            JSONObject j = new JSONObject();
            j.put("serverId", serverId);
            j.put("version", version);
            String data = RSAUtils.encrypt(j.toJSONString(), config.getPublic_key());

            Map<String, Object> map = new HashMap<>();
            map.put("gameName", gameName);
            map.put("data", data);
            map.put("sign", sign);
            rtn = HttpClient.doPost(config.getDecrypt_url(), map);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return rtn;
    }


    /**
     * 生成一个 容器的类扫描地址
     * @return
     */
    public static URL createContainerURL(){
        URL u = null;
        try {
            u = new URL(ContainerApp.CONTAINER_TYPE, null, -1, "",new ContainerHandler());
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return u;
    }
}

  3 使用Reflections扫描容器自定义加载器中的class

        Reflections reflections = null;
        // 容器启动 添加容器扫描地址
        if(ContainerApp.getLoader() != null){
            reflections = new Reflections(packagePath, ContainerApp.createContainerURL(), ContainerApp.getLoader());
        }else{
            reflections = new Reflections(packagePath);
        }

        Set<Class<? extends IBizHandler>> classes = reflections.getSubTypesOf(IBizHandler.class);

     这里我们需要注意,如果只 是这样使用 Reflections reflections = new Reflections(packagePath);的话,是无法扫描到容器中加载的class的,这里看Reflections的构造函数片段,传入不同的参数的不同加载逻辑

while(var7.hasNext()) {
            param = var7.next();
            if (param instanceof String) {
                builder.addUrls(ClasspathHelper.forPackage((String)param, classLoaders));
                filter.includePackage(new String[]{(String)param});
            } else if (param instanceof Class) {
                if (Scanner.class.isAssignableFrom((Class)param)) {
                    try {
                        builder.addScanners((Scanner)((Class)param).newInstance());
                    } catch (Exception var11) {
                    }
                }

                builder.addUrls(ClasspathHelper.forClass((Class)param, classLoaders));
                filter.includePackage((Class)param);
            } else if (param instanceof Scanner) {
                scanners.add((Scanner)param);
            } else if (param instanceof URL) {
                builder.addUrls((URL)param);
            } else if (!(param instanceof ClassLoader)) {
                if (param instanceof Predicate) {
                    filter.add((Predicate)param);
                } else if (param instanceof ExecutorService) {
                    builder.setExecutorService((ExecutorService)param);
                } else if (Reflections.log != null) {
                    throw new ReflectionsException("could not use param " + param);
                }
            }
        }

     对于只传入String类型的path,会进行如下加载

public static Collection<URL> forResource(String resourceName, ClassLoader... classLoaders) {
        List<URL> result = new ArrayList();
        ClassLoader[] loaders = classLoaders(classLoaders);
        ClassLoader[] var4 = loaders;
        int var5 = loaders.length;

        for(int var6 = 0; var6 < var5; ++var6) {
            ClassLoader classLoader = var4[var6];

            try {
                Enumeration urls = classLoader.getResources(resourceName);

                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    int index = url.toExternalForm().lastIndexOf(resourceName);
                    if (index != -1) {
                        result.add(new URL(url.toExternalForm().substring(0, index)));
                    } else {
                        result.add(url);
                    }
                }
            } catch (IOException var11) {
                if (Reflections.log != null) {
                    Reflections.log.error("error getting resources for " + resourceName, var11);
                }
            }
        }

        return distinctUrls(result);
    }

调用了classloader的getresource方法去扫描class的路径,这个classloader哪来的?

    public static ClassLoader[] classLoaders(ClassLoader... classLoaders) {
        if (classLoaders != null && classLoaders.length != 0) {
            return classLoaders;
        } else {
            ClassLoader contextClassLoader = contextClassLoader();
            ClassLoader staticClassLoader = staticClassLoader();
            return contextClassLoader != null ? (staticClassLoader != null && contextClassLoader != staticClassLoader ? new ClassLoader[]{contextClassLoader, staticClassLoader} : new ClassLoader[]{contextClassLoader}) : new ClassLoader[0];
        }
    }

    这里可以看到,如果在构造函数传入了classloader就会使用自定的类加载器,如果没传入就会使用 上下文类加载器,而这个上下文类加载器默认是系统类加载器(不要想着去改上下文类加载器的默认加载器,会导致很多问题,spi依赖这个实现自己的功能,spring,jdbc什么的好多框架都会不好使),是我们自定义类加载器的父加载器,所以自定义加载器的路径就会扫描不到。到这里,你可能会想着在构造方法中传入自定义的类加载器,然后实现自定义类加载器的getResources()方法,提供一个URL对象,(这是一个思路,我尝试过这么做,但是ClassLoader的getResources()方法会被其他java类默认调用,并调用生成的URL中的一些connect和stream相关的方法,而这些方法对我们的需求来说并不是必须要实现的,实现起来也比较困难)

  我们看到,在Reflections的构造函数中还可以直接传入URL对象,我们可以以此为切入点进行分析,这个传入的URL是在什么时候被调用的:

 protected void scan() {
      

        for (final URL url : configuration.getUrls()) {
            try {
                if (executorService != null) {
                    futures.add(executorService.submit(new Runnable() {
                        public void run() {
                            if (log != null && log.isDebugEnabled()) log.debug("[" + Thread.currentThread().toString() + "] scanning " + url);
                            scan(url);
                        }
                    }));
                } else {
                    scan(url);
                }
                scannedUrls++;
            } catch (ReflectionsException e) {
                if (log != null && log.isWarnEnabled()) log.warn("could not create Vfs.Dir from url. ignoring the exception and continuing", e);
            }
        }


    }

构造函数中调用scan()方法扫描所有路径,去扫描里面的class文件

 

对于每个URLscan做如下处理: 1是加载URL,2是扫描File

    protected void scan(URL url) {
        Vfs.Dir dir = Vfs.fromURL(url);

        try {
            for (final Vfs.File file : dir.getFiles()) {
                // scan if inputs filter accepts file relative path or fqn
                Predicate<String> inputsFilter = configuration.getInputsFilter();
                String path = file.getRelativePath();
                String fqn = path.replace('/', '.');
                if (inputsFilter == null || inputsFilter.apply(path) || inputsFilter.apply(fqn)) {
                    Object classObject = null;
                    for (Scanner scanner : configuration.getScanners()) {
                        try {
                            if (scanner.acceptsInput(path) || scanner.acceptResult(fqn)) {
                                classObject = scanner.scan(file, classObject);
                            }
                        } catch (Exception e) {
                            if (log != null && log.isDebugEnabled())
                                log.debug("could not scan file " + file.getRelativePath() + " in url " + url.toExternalForm() + " with scanner " + scanner.getClass().getSimpleName(), e.getMessage());
                        }
                    }
                }
            }
        } finally {
            dir.close();
        }
    }

可以看到 Vfs.fromURL(url) 这句是加载URL路径的关键:

   private static List<UrlType> defaultUrlTypes = Lists.<UrlType>newArrayList(DefaultUrlTypes.values());


  public static Dir fromURL(final URL url) {
        return fromURL(url, defaultUrlTypes);
    }

    /** tries to create a Dir from the given url, using the given urlTypes*/
    public static Dir fromURL(final URL url, final List<UrlType> urlTypes) {
        for (UrlType type : urlTypes) {
            try {
                if (type.matches(url)) {
                    Dir dir = type.createDir(url);
                    if (dir != null) return dir;
                }
            } catch (Throwable e) {
                if (Reflections.log != null) {
                    Reflections.log.warn("could not create Dir using " + type + " from url " + url.toExternalForm() + ". skipping.", e);
                }
            }
        }

        throw new ReflectionsException("could not create Vfs.Dir from url, no matching UrlType was found [" + url.toExternalForm() + "]\n" +
                "either use fromURL(final URL url, final List<UrlType> urlTypes) or " +
                "use the static setDefaultURLTypes(final List<UrlType> urlTypes) or addDefaultURLTypes(UrlType urlType) " +
                "with your specialized UrlType.");
    }

内部实现遍历了默认的 URLTypes的列表,所以在启动自定义类加载器之前要自定义一个加载类型,具体如下:

            // 自定义容器一个加载类型,Reflections构造方法用
            ContainerType type = new ContainerType(new ContainerDir(fileList));
            Vfs.addDefaultURLTypes(type);

这里面涉及的一些接口都要自己实现:

package earth.pack.container.reflection;

import org.reflections.vfs.Vfs;
import java.util.ArrayList;
import java.util.List;

/**
 * @desc: 虚拟目录
 * @author zhangjiaqi
 * @time 2021/4/12 20:59
 */
public class ContainerDir implements Vfs.Dir {

    List<Vfs.File> list = new ArrayList<>();

    public ContainerDir(List<Vfs.File> list) {
        this.list = list;
    }

    @Override
    public String getPath() {
        return null;
    }

    @Override
    public Iterable<Vfs.File> getFiles() {
        return list;
    }

    @Override
    public void close() {

    }
}
package earth.pack.container.reflection;

import org.reflections.vfs.Vfs;

import java.io.IOException;
import java.io.InputStream;

/**
 * @desc: 虚拟文件
 * @author zhangjiaqi
 * @time 2021/4/12 20:59
 */
public class ContainerFile implements Vfs.File{
    String name;
    String path;
    InputStream in;


    public ContainerFile(String name, String path, InputStream in){
        this.name = name;
        this.path = path;
        this.in = in;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public String getRelativePath() {
        return path;
    }

    @Override
    public InputStream openInputStream() throws IOException {
        return in;
    }
}
package earth.pack.container.reflection;

import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.util.jar.JarFile;

/**
 * @desc: 虚拟文件流处理
 * @author zhangjiaqi
 * @time 2021/4/12 21:00
 */
public class ContainerHandler extends URLStreamHandler {

    @Override
    protected URLConnection openConnection(URL u) throws IOException {
        return new JarURLConnection(u) {
            @Override
            public JarFile getJarFile() throws IOException {
                return null;
            }

            @Override
            public void connect() throws IOException {

            }
        };
    }
}
package earth.pack.container.reflection;

import earth.pack.container.ContainerApp;
import org.reflections.vfs.Vfs;

import java.net.URL;

/**
 * @desc: 容器扫描地址
 * @author zhangjiaqi
 * @time 2021/4/12 21:00
 */
public class ContainerType implements Vfs.UrlType {


    Vfs.Dir dir;

    public ContainerType(Vfs.Dir dir){
        this.dir = dir;
    }

    @Override
    public boolean matches(URL url) throws Exception {
        return url.getProtocol().indexOf(ContainerApp.CONTAINER_TYPE) != -1;
    }

    @Override
    public Vfs.Dir createDir(URL url) throws Exception {
        return dir;
    }
}

这些实现的关键是 ContainerDir 类里面的 List<Vfs.File> list,这是解密后的二进制流,而Reflections中的scan正是通过File的openStream()方法获取class的二进制流,去定义Class文件。

    public ClassFile getOfCreateClassObject(File file) {
        InputStream inputStream = null;

        ClassFile var4;
        try {
            inputStream = file.openInputStream();
            DataInputStream dis = new DataInputStream(new BufferedInputStream(inputStream));
            var4 = new ClassFile(dis);
        } catch (IOException var8) {
            throw new ReflectionsException("could not create class file from " + file.getName(), var8);
        } finally {
            Utils.close(inputStream);
        }

        return var4;
    }

至此,一个自定义的类加载器已经实现,也通过了解底层,实现了自定义的URL,实现Reflections扫描自定义的类加载器中加载的class。



这篇关于自定义类加载器加载加密jar包,使用Reflections扫描自定义加载器加载的Class的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程