Java 将项目打包成Jar,并在Jar中使用来自外部的第三方Jar包,而不是直接依赖(bcprov-jdk15on)

一. 背景

        最近需要将一项加解密功能从Web应用中剥离,制作成一个独立可执行的Jar包,供客户离线使用。加解密时使用到了bcprov轻量级加密API,这个Jar包在运行时会检索签名,比对自身包含的文件大小,若有任何一项出现异常,则运行时直接报错:

java.lang.SecurityException: JCE cannot authenticate the provider BC

        由于我使用的maven打包插件是maven-shade-plugin,在设置了createDependencyReducedPom参数值为false后,为了避免重复依赖已有模块,会对第三方依赖解压再压缩,这个过程会导致META-INF中的5份文件的大小发生变化。所以我想到不能直接通过Maven pom.xml进行依赖,而是在程序运行时动态的将需要的Jar(bcprov-jdk15on.jar)通过类加载器URLClassLoader加载至JVM中。但是问题接踵而来,第三方Jar(bcprov-jdk15on.jar)存放在哪里?如何获取URLClassLoader加载Jar时需要的URL?

       针对第一个问题,我决定将第三方Jar放在Resource目录下,这是因为不能向客户暴露过多有关加解密的实现细节,并且也方便客户使用(如若不然,就需要提供项目Jar和第三方加解密Jar两个Jar包了)。

       针对第二个问题,由于Resource目录内的资源在编译后存放在Classpath中,我曾尝试使用ClassLoader getResourceAsStream(String fileName)的方式获取Jar的Inputstream,将其输出到物理磁盘上任意位置(生成一份新的Jar),最后通过file.toURI().toURL()来获取URL。遗憾的是,最终读取新的Jar的META-INF目录中,MANIFEST.MF文件的CRC SHA发生了改变,导致加解密时运行时报错:

java.lang.SecurityException: Invalid signature file digest for Manifest main attributes

    无效的数字签名。

    因此,我最终的做法是:在项目运行时,直接将携带第三方Jar的完整项目Jar包进行解压缩,这样得到的第三方Jar没有任何损坏。

二. 实现方式

    1. 构建Jar加载器

//Jar加载器
JarLoader jarLoader = new JarLoader((URLClassLoader) ClassLoader.getSystemClassLoader());

    2. 临时目录,解压缩的文件将被暂时放置在临时目录内 为了尽可能的避免用户看到加解密细节,我将bc解压在用户的临时目录下。

String systemTempPath = System.getProperties().getProperty("java.io.tmpdir");

    3. 既然想解压项目完整Jar,那么首先应该定位到这个Jar。我的做法是以当前文件为基准点来进行定位,通过getProtectionDomain().getCodeSource().getLocation().getPath()来获取jar包的绝对路径(ps: 不要直接获取当前文件的路径,因因为当前的class文件封装在Jar包内,路径是类似jar:file:/.../.../xxx.jar!/com.c1的相对路径,而不是file://C://xxx.jar 这种绝对路径)。

       注意: 一定要在加解密操作之前完成第三方jar对jvm的注入工作,否则在加密接时会报错 依赖的类(我这里是bc)找不到。

  try {
            //准备工作 将bcprov-jdk15on-1.57.jar加载至jvm中
            try {
                //当前文件所在jar包的物理路径   LocalDecrypt.class 我这份java文件的类名
                String currentJarPath = LocalDecrypt.class.getProtectionDomain().getCodeSource().getLocation().getPath();
                //当前文件所在目录的物理路径
                String parentPath = new File(currentJarPath).getParentFile().getPath();
                String unZipInput = parentPath.concat(File.separator).concat("项目Jar包的完整名称");
                String unZipOutput = systemTempPath.concat(File.separator);
                unZip(new File(unZipInput), new File(unZipOutput));
                // 加载Jar
                JarLoader.loadjar(jarLoader, systemTempPath, "bcprov-jdk15on-1.57.jar");
            } catch (Exception e) {
                throw new Exception("jarLoader加载时出现异常: " + e.toString());
            }

            for (String cipher : ciphers) {
                String decrypt = null;
                String errorMsg = null;
                for (String privateKey : privateKeys) {
                    try {
                        // 密码解密
                        decrypt = DaemonSupport.decrypt(cipher, privateKey);
                    } catch (Exception e) {
                        errorMsg = e.toString();
                        //System.out.println(e.toString());
                    }
                }
                if (null == decrypt) {
                    System.out.println(String.format("密文: %s, 私钥: %s 解密时出错,原因: %s", cipher, privateKeys.toString(), errorMsg));
                }
                cleartextMap.put(cipher, decrypt);
            }
            return cleartextMap;
        } finally {
            File file = new File(systemTempPath.concat(File.separator).concat("bcprov-jdk15on-1.57.jar"));
            if (file.exists()) {
                file.delete();
            }
        }

       4. Jar包装载器

public class JarLoader {
    private URLClassLoader urlClassLoader;

    public JarLoader(URLClassLoader urlClassLoader) {
        this.urlClassLoader = urlClassLoader;
    }

    public void loadJar(URL url) throws Exception {
        Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
        addURL.setAccessible(true);
        addURL.invoke(urlClassLoader, url);
    }

    public static void loadjar(JarLoader jarLoader, String path, String targetFileName) throws Exception {
        File libdir = new File(path);
        if (libdir != null && libdir.isDirectory()) {
            // 对目录下的文件进行过滤,只保留后缀为.jar的文件
            File[] listFiles = libdir.listFiles(file -> {
                return file.exists() && file.isFile() && file.getName().endsWith(".jar");
            });

            for (File file : listFiles) {
                if(file.getName().equals(targetFileName)) {
                    jarLoader.loadJar(file.toURI().toURL());
                }
            }

        } else {
            throw new Exception("目标Jar包路径不存在");
        }
    }
}

 

你可能感兴趣的:(Java 将项目打包成Jar,并在Jar中使用来自外部的第三方Jar包,而不是直接依赖(bcprov-jdk15on))