spring-boot-maven-plugin详解,如何将spring-boot-loader打到jar包

时间:2024-03-17 13:01:23

       最近针对代码安全保护需求进行技术调研,因为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的文件,如下图:

spring-boot-maven-plugin详解,如何将spring-boot-loader打到jar包

     那么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依赖包,打开依赖包,可以发现结构如下:

spring-boot-maven-plugin详解,如何将spring-boot-loader打到jar包

通过这个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包中,如下图:

spring-boot-maven-plugin详解,如何将spring-boot-loader打到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包中的最底层的方法实现,spring-boot-maven-plugin详解,如何将spring-boot-loader打到jar包

整个分析过程已经结束,如果想了解更多,静待下回分享。