最近针对代码安全保护需求进行技术调研,因为java文件编译成为class之后,可以通过反编译工具jd查看代码的逻辑,以及执行过程。为了防止class文件被反编译,调研了多种处理方案,其中最常见的就是代码混淆和class文件加密。目前proguard做的还不错,相关文章也比较多,但是复杂度还是有的,可以自行了解。接下要说的就是class文件加密技术,可以采用对称加密和非对称加密,算法也有很多种,对称加密一般采用AES,目前采用AES。
那么对class文件加密后,在什么地方解密呢?一般是在内存解密,即在classloader加载类的时候解密。此处需要了解classloader加载类的机制和过程。之前项目打成的war包,所以专门定制了一个tomcat,以此保护代码,但是只要找到类加载的地方,也是可以解密的。引申一下,如果想要保护的更加安全,就需要修改native方法,即定制jvm,修改c代码,这样**的难度就非常大了。另外的方式,也可以采用加密狗,通过加密狗的方式进行保护。
目前对springboot的包进行加密,采用xjar,在GitHub上开源,也是比较活跃的,通过源码分析,其实也就是自己实现了一个classloader,然后classloader对加载的class进行解密。对于代码保护还是比较有用的,可以自行了解一下。
我在分析xjar的源码过程中,顺带就分析了一下spring-boot-maven-plugin,这个插件是对springboot打成可运行的jar包,通过jar包分析,我们可以看到多了spring-boot-loader的文件,如下图:
那么loader是从何而来,有没有在maven中引入loader的工程。通过springboot的文档了解,该loader是通过spring-boot-maven-plugin插件打包进来的,那么接下来就分析一下这个插件。
首先在springboot工程的pom文件中额外引入如下的依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-loader-tools</artifactId> <version>2.1.6.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-plugin-api</artifactId> <version>3.5.0</version> </dependency> <dependency> <groupId>org.apache.maven.plugin-tools</groupId> <artifactId>maven-plugin-annotations</artifactId> <version>3.5</version> <scope>provided</scope> </dependency>
从上面的依赖,可以看出,基本都是插件需要的依赖。然后再idea中找到spring-boot-maven-plugin依赖包,打开依赖包,可以发现结构如下:
通过这个jar包,可以看到里面有很多mojo,关于maven中plugin的mojo相关知识,可以自行了解。可以看到有一个RepackageMojo的类,进入这个类,并查看execute()方法,以下只列出主要方法,如下:
@Override public void execute() throws MojoExecutionException, MojoFailureException { if (this.project.getPackaging().equals("pom")) { getLog().debug("repackage goal could not be applied to pom project."); return; } if (this.skip) { getLog().debug("skipping repackaging as per configuration."); return; } //此处是重新打包 repackage(); } private void repackage() throws MojoExecutionException { Artifact source = getSourceArtifact(); File target = getTargetFile(); Repackager repackager = getRepackager(source.getFile()); Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters())); Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog()); try { LaunchScript launchScript = getLaunchScript(); //重新打包工具类型 repackager.repackage(target, libraries, launchScript); } catch (IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } updateArtifact(source, target, repackager.getBackupFile()); }
接下来,查看Repackager 类的repackage方法,此时发现进入到另外的一个jar包中,如下图:
继续分析repackage方法,代码如下:
/** * Repackage to the given destination so that it can be launched using ' * {@literal java -jar}'. * @param destination the destination file (may be the same as the source) * @param libraries the libraries required to run the archive * @param launchScript an optional launch script prepended to the front of the jar * @throws IOException if the file cannot be repackaged * @since 1.3.0 */
public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException {
if (destination == null || destination.isDirectory()) {
throw new IllegalArgumentException("Invalid destination");
}
if (libraries == null) {
throw new IllegalArgumentException("Libraries must not be null");
}
if (this.layout == null) {
this.layout = getLayoutFactory().getLayout(this.source);
}
destination = destination.getAbsoluteFile();
File workingSource = this.source;
if (alreadyRepackaged() && this.source.equals(destination)) {
return;
}
if (this.source.equals(destination)) {
workingSource = getBackupFile();
workingSource.delete();
renameFile(this.source, workingSource);
}
destination.delete();
try {
try (JarFile jarFileSource = new JarFile(workingSource)) {
//对jar文件进行重新打包
repackage(jarFileSource, destination, libraries, launchScript);
}
}
finally {
if (!this.backupSource && !this.source.equals(workingSource)) {
deleteFile(workingSource);
}
}
}
private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript)
throws IOException {
WritableLibraries writeableLibraries = new WritableLibraries(libraries);
try (JarWriter writer = new JarWriter(destination, launchScript)) {
writer.writeManifest(buildManifest(sourceJar));
//这个地方是重点了,loader就是通过这个地方打入到jar包中
writeLoaderClasses(writer);
if (this.layout instanceof RepackagingLayout) {
writer.writeEntries(sourceJar,
new RenamingEntryTransformer(((RepackagingLayout) this.layout).getRepackagedClassesLocation()),
writeableLibraries);
}
else {
writer.writeEntries(sourceJar, writeableLibraries);
}
writeableLibraries.write(writer);
}
}
进入writeLoaderClasses(writer)方法,如下:
private void writeLoaderClasses(JarWriter writer) throws IOException { if (this.layout instanceof CustomLoaderLayout) { ((CustomLoaderLayout) this.layout).writeLoadedClasses(writer); } else if (this.layout.isExecutable()) { writer.writeLoaderClasses(); } }
再继续进入writer.writeLoaderClasses()方法,如下:
/** * Write the required spring-boot-loader classes to the JAR. * @throws IOException if the classes cannot be written */ @Override public void writeLoaderClasses() throws IOException { //需要加载的loader包的位置 writeLoaderClasses(NESTED_LOADER_JAR); }
/** * Write the required spring-boot-loader classes to the JAR. * @param loaderJarResourceName the name of the resource containing the loader classes * to be written * @throws IOException if the classes cannot be written */ @Override public void writeLoaderClasses(String loaderJarResourceName) throws IOException { URL loaderJar = getClass().getClassLoader().getResource(loaderJarResourceName); try (JarInputStream inputStream = new JarInputStream(new BufferedInputStream(loaderJar.openStream()))) { JarEntry entry; while ((entry = inputStream.getNextJarEntry()) != null) { if (entry.getName().endsWith(".class")) { writeEntry(new JarArchiveEntry(entry), new InputStreamEntryWriter(inputStream)); } } } }
至此就是将loader加入到jar包中的最底层的方法实现,
整个分析过程已经结束,如果想了解更多,静待下回分享。