spring-boot-maven-plugin詳解,如何將spring-boot-loader打到jar包

       最近針對代碼安全保護需求進行技術調研,因爲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包中的最底層的方法實現,

整個分析過程已經結束,如果想了解更多,靜待下回分享。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章