自定义类加载器加载加密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的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-25【机器学习(二)】分类和回归任务-决策树(Decision Tree,DT)算法-Sentosa_DSML社区版
- 2024-11-23增量更新怎么做?-icode9专业技术文章分享
- 2024-11-23压缩包加密方案有哪些?-icode9专业技术文章分享
- 2024-11-23用shell怎么写一个开机时自动同步远程仓库的代码?-icode9专业技术文章分享
- 2024-11-23webman可以同步自己的仓库吗?-icode9专业技术文章分享
- 2024-11-23在 Webman 中怎么判断是否有某命令进程正在运行?-icode9专业技术文章分享
- 2024-11-23如何重置new Swiper?-icode9专业技术文章分享
- 2024-11-23oss直传有什么好处?-icode9专业技术文章分享
- 2024-11-23如何将oss直传封装成一个组件在其他页面调用时都可以使用?-icode9专业技术文章分享
- 2024-11-23怎么使用laravel 11在代码里获取路由列表?-icode9专业技术文章分享