Maven构建SpringBoot项目加密Jar包

Maven构建SpringBoot项目加密Jar包,第1张

Maven构建SpringBoot项目加密Jar包

目录

摘要

整体描述

过程分解

1、创建动态链接库

2、 SpringBoot 生成可执行 jar 包

3、 Maven 自定义插件

3.3.1、 自定义插件

3.3.2、 创建Jar 包加密工具

3.3.3、 插件与加密工具整合

3.3.4、插件打成依赖 Jar 包

4、插件应用

参考文献


摘要

        由于业务需要,需要将提供给客户的可执行 Jar 包源码加密处理;基本要求:不能改变原有的逻辑和代码结构;团队研发人员无感知。最终实现过程可以概括为:

        1、使用Maven工具,执行打包命令,生成加密 jar 包;

        2、java -jar 命令直接运行加密后的 jar 包。

        本文章参考了 Xjar 项目的加密方式,结合自定义 Maven 插件和JVMTI,最终实现了上述过程。

整体描述

1、SpringBoot 项目使用 Maven 工具自动构建可执行 jar 包;

2、(主要过程)实现加密 jar 包的过程本质上是将 Maven 构建的可执行 jar 包进一步封装,对 jar 包中的 class 文件进行加密处理;

Tip: 具体的加密算法根据不同需求场景自由定义

3、C/C++ 创建动态链接库 encrypt.dll(window平台,linux平台为 encrypt.so),通过以下命令参数匹配动态链接库,并实现 class 文件的解密运行过程

java -agentlib:encrypt -jar your-executeable-jar-file.jar

过程分解 1、创建动态链接库

windows

gcc -shared encrypt.c -o encrypt.dll -I "{JAVA_HOME}/include" -I "{JAVA_HOME}/include/win32"

linux

gcc -shared encrypt.c -o encrypt.so -I "{JAVA_HOME}/include" -I "{JAVA_HOME}/include/linux"

将通过以上指令生成的动态链接库文件(.dll 或 .so)放到系统路径下;

Java 可通过以下方式查看系统路径:

System.getProperty("java.library.path")

encrypt.c 文件:

由 C/C++ 语言编写的代码,用于生成动态链接库,通过 java agent 方式实现 class 文件解密过程

完整代码

#include 
#include 
#include "jni.h"
#include 
#include 

// 加密方法
void encode(char *str) {
    unsigned int len = strlen(str);
    for (int i = 0; i < len; i ++) {
        str[i] = str[i] ^ 0x07;
    }
}

// jni 接口实现
JNIEXPORT jbyteArray JNICALL Java_com_wyhw_plugin_encrypt_encrypt_JarEncryptUtil_encrypt(JNIEnv * env, jclass cls, jbyteArray bytes) {
    char* dst = (char *) ((*env) -> GetByteArrayElements(env, bytes, 0));
    encode(dst);
    (*env) -> SetByteArrayRegion(env, bytes, 0, strlen(dst), (jbyte *) dst);
    return bytes;
}

// class 文件解密
void JNICALL ClassDecryptHook(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jclass class_being_redefined, jobject loader, const char* name,
      jobject protection_domain, jint class_data_len, const unsigned char* class_data, jint* new_class_data_len, unsigned char** new_class_data) {
	
	//printf("ClassDecryptHook Files: %sn", (char *) name);
    *new_class_data_len = class_data_len;
    (*jvmti_env) -> Allocate(jvmti_env, class_data_len, new_class_data);
    unsigned char* _data = *new_class_data;
	if (name && strncmp(name, "com/zk/sdk", 10) == 0 && strstr(name, "$$") == NULL) {
		//printf("nnhit: %sn", (char *) name);
		//printf("len=%dnn", class_data_len);
		for (int i = 0; i < class_data_len; ++i) {
			_data[i] = class_data[i];
		}
		encode(_data);
	} else {
		for (int i = 0; i < class_data_len; ++i) {
			_data[i] = class_data[i];
		}
	}
}

JNIEXPORT jint JNICALL Agent_onLoad(JavaVM *vm, char *options, void *reserved) {
    jvmtiEnv *jvmtiEnv;
    jint ret = (*vm) -> GetEnv(vm, (void **)&jvmtiEnv, JVMTI_VERSION);
    if (JNI_OK != ret) {
        printf("ERROR: Fail get jvmtiEvn!n");
        return ret;
    }
    jvmtiCapabilities jvmtiCapabilities;
    (void) memset(&jvmtiCapabilities, 0, sizeof(jvmtiCapabilities));
    jvmtiCapabilities.can_generate_all_class_hook_events = 1;
    jvmtiCapabilities.can_tag_objects = 1;
    jvmtiCapabilities.can_generate_object_free_events = 1;
    jvmtiCapabilities.can_get_source_file_name = 1;
    jvmtiCapabilities.can_get_line_numbers = 1;
    jvmtiCapabilities.can_generate_vm_object_alloc_events = 1;

    jvmtiError error = (*jvmtiEnv) -> AddCapabilities(jvmtiEnv, &jvmtiCapabilities);
    if (JVMTI_ERROR_NONE != error) {
        printf("ERROR: Unable to AddCapabilities JVMTI!n");
        return error;
    }

    jvmtiEventCallbacks callbacks;
    (void) memset(&callbacks, 0, sizeof(callbacks));

    callbacks.ClassFileLoadHook = &ClassDecryptHook;
    error = (*jvmtiEnv) -> SetEventCallbacks(jvmtiEnv, &callbacks, sizeof(callbacks));
    if (JVMTI_ERROR_NONE != error) {
        printf("ERROR: Fail setEventCallBacks!n");
        return error;
    }

    error = (*jvmtiEnv) -> SetEventNotificationMode(jvmtiEnv, JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL);
    if (JVMTI_ERROR_NONE != error) {
        printf("ERROR: Fail SetEventNotificationMode!n");
        return error;
    }
    return JNI_OK;
}
2、 SpringBoot 生成可执行 jar 包

在项目 pom.xml 文件中添加 spring-boot-maven-plugin 插件


    
        
            org.springframework.boot
            spring-boot-maven-plugin
        
    

 之后会在 Maven 插件处出现 spring-boot 插件

 运行 mvn clean package 命令,生成可执行 jar 包(一般放在项目 target 目录下)

Tip:Maven 命令 clean 和 package 所涉及的生命周期不同,可以参考相关文档 参考文献

3、 Maven 自定义插件

        本插件的目标是对 spring-boot-maven-plugin 插件生成的可执行 jar 包进行二次处理,实现 class 字节码文件加密处理。

3.3.1、 自定义插件

引入 pom 依赖 


	org.apache.maven.plugin-tools
	maven-plugin-annotations
	3.4
	provided



	org.apache.maven
	maven-core
	2.2.1

1、创建自定义 Mojo 类 EncryptMojo;

2、继承 org.apache.maven.plugin.AbstractMojo 抽象类;

3、类文件需要添加 org.apache.maven.plugins.annotations.Mojo 注解

4、实现 execute() 方法(加密过程就是在这里实现的)

EncryptMojo.java 完整代码

package com.wyhw.plugins.plugin.encrypt;

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;


@Mojo(
		name = "encrypt",
		defaultPhase = LifecyclePhase.PACKAGE,
		requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME
)
public class EncryptMojo extends AbstractMojo {

	@Parameter(defaultValue = "${project}", readonly = true)
	private MavenProject project;

	
	@Parameter(property = "encrypt.on", defaultValue = "true")
	private boolean on;

	
	@Parameter(property = "encrypt.over", defaultValue = "false")
	private boolean over;

	
	@Parameter(property = "encrypt.suffix", defaultValue = "-entry")
	private String archiveSuffix;

	public void setOn(String on) {
		this.on = Boolean.valueOf(on);
	}

	public void setOver(String over) {
		this.over = Boolean.valueOf(over);
	}

	public void setArchiveSuffix(String archiveSuffix) {
		this.archiveSuffix = archiveSuffix;
	}

	public void execute() throws MojoExecutionException, MojoFailureException {
		Log log = getLog();
		if (!on) {
			log.info("Disable encrypt package !!!");
			return;
		}
		String originalArchivePath = project.getArtifact().getFile().getAbsolutePath();
		log.info("Original file path: " + originalArchivePath);
		int lastSeparatorIndex = originalArchivePath.lastIndexOf(".");
		String fileName = originalArchivePath.substring(0, lastSeparatorIndex);
		String extendName = originalArchivePath.substring(lastSeparatorIndex + 1);
		String newFileName = fileName + archiveSuffix + "." + extendName;
		log.info("Encrypt file path: " + newFileName);
		try {
			if (over) {
				log.info("Overwrite original file");
				newFileName = originalArchivePath;
			}
			JarEncryptUtil.encrypt(originalArchivePath, newFileName);
		} catch (Exception e) {
			log.error("自动加密失败", e);
		}
	}
}
3.3.2、 创建Jar 包加密工具

引入 pom 依赖


	org.apache.commons
	commons-compress
	LATEST

直接上代码

 JarEncryptUtil .java

package com.wyhw.plugins.plugin.encrypt;

import org.apache.commons.compress.archivers.jar.JarArchiveEntry;
import org.apache.commons.compress.archivers.jar.JarArchiveInputStream;
import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream;

import java.io.*;
import java.util.jar.Manifest;
import java.util.logging.Logger;


public class JarEncryptUtil {

	static {
		System.loadLibrary("encrypt");
	}

	private static Logger logger = Logger.getGlobal();

	public native static byte[] encrypt(byte[] bytes);

	public static void encrypt(String src, String desc) throws Exception {
		File srcFile = new File(src);
		File descFile = new File(desc);
		try (FileInputStream fis = new FileInputStream(srcFile);
		     FileOutputStream fos = new FileOutputStream(descFile);
		     JarArchiveInputStream jis = new JarArchiveInputStream(fis);
		     JarArchiveOutputStream jos = new JarArchiveOutputStream(fos);) {

			UnclosedInputStream nis = new UnclosedInputStream(jis);
			UnclosedOutputStream nos = new UnclosedOutputStream(jos);

			JarArchiveEntry jarArchiveEntry;
			while ((jarArchiveEntry = jis.getNextJarEntry()) != null) {
				long time = jarArchiveEntry.getTime();
				String name = jarArchiveEntry.getName();
				JarArchiveEntry descJarEntry = new JarArchiveEntry(name);
				descJarEntry.setTime(time);
				if (jarArchiveEntry.isDirectory()) {
					jos.putArchiveEntry(descJarEntry);
				} else if (name.equals("meta-INF/MANIFEST.MF")) {
					Manifest manifest = new Manifest(nis);
					jos.putArchiveEntry(descJarEntry);
					manifest.write(nos);
				} else if (name.startsWith("BOOT-INF/classes/")) {
					jos.putArchiveEntry(descJarEntry);
					if (name.contains("com/zk/") && name.endsWith(".class")) {
						transfer(encryptCus(nis), nos);
						logger.info(name + " 文件已加密");
					} else {
						transfer(nis, nos);
					}
				} else if (name.startsWith("BOOT-INF/lib/")) {
					descJarEntry.setMethod(JarArchiveEntry.STORED);
					descJarEntry.setSize(jarArchiveEntry.getSize());
					descJarEntry.setCrc(jarArchiveEntry.getCrc());
					jos.putArchiveEntry(descJarEntry);
					transfer(nis, nos);
				} else {
					jos.putArchiveEntry(descJarEntry);
					transfer(nis, nos);
				}
				jos.closeArchiveEntry();
			}
			jos.finish();
		}
	}

	private static InputStream encryptCus(InputStream nis) throws IOException {
		try (BufferedInputStream bis = new BufferedInputStream(nis);
		     ByteArrayOutputStream bos = new ByteArrayOutputStream();) {
			byte[] buffer = new byte[4096];
			int len;
			while ((len = bis.read(buffer)) != -1) {
				bos.write(buffer, 0, len);
			}
			bos.flush();
			byte[] bytes = bos.toByteArray();
			encrypt(bytes);
			return new ByteArrayInputStream(bytes);
		}
	}

	
	private static void transfer(InputStream in, OutputStream out) throws IOException {
		byte[] buffer = new byte[4096];
		int length;
		while ((length = in.read(buffer)) != -1) {
			out.write(buffer, 0, length);
		}
		out.flush();
	}
}

UnclosedOutputStream.java

package com.wyhw.plugins.plugin.encrypt;

import java.io.IOException;
import java.io.OutputStream;


public class UnclosedOutputStream extends OutputStream {
    private final OutputStream out;

    public UnclosedOutputStream(OutputStream out) {
        this.out = out;
    }

    @Override
    public void write(int b) throws IOException {
        out.write(b);
    }

    @Override
    public void write(byte[] b) throws IOException {
        out.write(b);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        out.write(b, off, len);
    }

    @Override
    public void flush() throws IOException {
        out.flush();
    }

    @Override
    public void close() {
    }
}

UnclosedInputStream .java

package com.wyhw.plugins.plugin.encrypt;

import java.io.IOException;
import java.io.InputStream;


public class UnclosedInputStream extends InputStream {
    private final InputStream in;

    public UnclosedInputStream(InputStream in) {
        this.in = in;
    }

    @Override
    public int read() throws IOException {
        return in.read();
    }

    @Override
    public int read(byte[] b) throws IOException {
        return in.read(b);
    }

    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        return in.read(b, off, len);
    }

    @Override
    public long skip(long n) throws IOException {
        return in.skip(n);
    }

    @Override
    public int available() throws IOException {
        return in.available();
    }

    @Override
    public void mark(int readLimit) {
        in.mark(readLimit);
    }

    @Override
    public void reset() throws IOException {
        in.reset();
    }

    @Override
    public boolean markSupported() {
        return in.markSupported();
    }

    @Override
    public void close() {
    }
}
3.3.3、 插件与加密工具整合

在 execute() 方法中直接调用加密工具方法: 

JarEncryptUtil.encrypt(originalArchivePath, newFileName);
3.3.4、插件打成依赖 Jar 包

需要加密的项目在使用自定义 Maven 插件时,需要引入插件 Jar 包;所以需要将此插件打包成供第三方项目依赖的 Jar 包

引入打包插件:


	
		
			maven-plugin-plugin
			3.6.0
			
				
					default-addPluginArtifactmetadata
					package
					
						addPluginArtifactmetadata
					
				
				
					default-descriptor
					process-classes
					
						descriptor
					
				
			
		
	

执行 maven 命令:

mvn clean package

至此,已经完成了加密插件的定义;

4、插件应用

在需要加密的第三方项目 pom.xml 文件中引入依赖:


	com.wyhw.plugin
    encrypt-maven-plugin
	0.0.1-SNAPSHOT

配置打包插件

在 spring-boot-maven-plugin 插件之后配置自定义加密插件(spring-boot-maven-plugin 插件和自定义的加密插件都是处于 Maven 生命周期中 package 阶段的插件,插件执行顺序跟配置的前后顺序一致;只有先执行 spring-boot-maven-plugin 插件,再执行自定义加密插件,加密过程才能正常执行)


	
		org.springframework.boot
		spring-boot-maven-plugin
	

   
		com.wyhw.plugin
        encrypt-maven-plugin
		0.0.1-SNAPSHOT
		
			
				
					encrypt
				
			
		
	

执行 package 命令,生成加密 jar 包

mvn clean package

 加密成功后,控制台会打印出相应文件加密成功的日志信息

"C:Program FilesJavajdk1.8.0_211binjava.exe" -Dmaven.multiModuleProjectDirectory=E:spaceideasdk "-Dmaven.home=D:IntelliJ IDEA 2019.1.2pluginsmavenlibmaven3" "-Dclassworlds.conf=D:IntelliJ IDEA 2019.1.2pluginsmavenlibmaven3binm2.conf" "-javaagent:D:IntelliJ IDEA 2019.1.2libidea_rt.jar=57987:D:IntelliJ IDEA 2019.1.2bin" -Dfile.encoding=UTF-8 -classpath "D:IntelliJ IDEA 2019.1.2pluginsmavenlibmaven3bootplexus-classworlds-2.5.2.jar" org.codehaus.classworlds.Launcher -Didea.version2019.1.2 -DskipTests=true package
[INFO] Scanning for projects...
[INFO]                                                                         
[INFO] ------------------------------------------------------------------------
[INFO] Building sdk 0.0.1-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ sdk ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] Copying 1 resource
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ sdk ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ sdk ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Using 'UTF-8' encoding to copy filtered properties files.
[INFO] skip non existing resourceDirectory E:spaceideasdksrctestresources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ sdk ---
[INFO] Nothing to compile - all classes are up to date
[INFO] 
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ sdk ---
[INFO] Tests are skipped.
[INFO] 
[INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ sdk ---
[INFO] Building jar: E:spaceideasdktargetsdk-0.0.1-SNAPSHOT.jar
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.5.3:repackage (repackage) @ sdk ---
[INFO] Replacing main artifact with repackaged archive
[INFO] 
[INFO] --- zk-common-plugins:0.0.1-SNAPSHOT:encrypt (default) @ sdk ---
[INFO] Original file path: E:spaceideasdktargetsdk-0.0.1-SNAPSHOT.jar
[INFO] Encrypt file path: E:spaceideasdktargetsdk-0.0.1-SNAPSHOT-encrypt.jar
一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt
信息: BOOT-INF/classes/com/zk/sdk/core/util/CRC32Util.class 文件已加密
一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt
信息: BOOT-INF/classes/com/zk/sdk/core/wrapper/RGBTriple.class 文件已加密
一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt
信息: BOOT-INF/classes/com/zk/sdk/SdkApplication.class 文件已加密
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.235 s
[INFO] Finished at: 2022-01-04T19:11:42+08:00
[INFO] Final Memory: 27M/408M
[INFO] ------------------------------------------------------------------------

Process finished with exit code 0
参考文献

JVMTI 官方文档:JVM(TM) Tool Interface 1.2.3

XJar 开源项目:GitHub - core-lib/xjar: Spring Boot JAR 安全加密运行工具,支持的原生JAR。

Maven: Maven 的生命周期

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5696580.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存