最近需要将一项加解密功能从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包路径不存在");
}
}
}