侧边栏壁纸
博主头像
术业有道之编程博主等级

亦是三月纷飞雨,亦是人间惊鸿客。亦是秋霜去叶多,亦是风华正当时。

  • 累计撰写 99 篇文章
  • 累计创建 50 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

教你将java程序做成黑盒并跨平台分发

Administrator
2022-01-24 / 0 评论 / 0 点赞 / 246 阅读 / 34167 字

写在前面

本文是一篇教你如何将java程序可执行jar做成黑盒程序,防止反编译泄漏源代码(是防止反编译,不是混淆编译),并防止通过jvm内存调试jvm字节码注入jconsole等方式对程序进行监控和操作。

友情提醒:这篇文章涉及到的技术点和操作比较多,看官们尽量不要跳着看,容易迷路。

用到的技术栈及工具

  • 环境:
    • golang 、java、shell 、jdk(当前项目所需要的版本即可)
  • 工具:
    • io.xjar 、jdk14(不是必须)

实现思路

  • 第一步:项目使用io.xjar加密正常编译得到的原始jar,得到一个加密的jar,和一个启动器go文件
  • 第二步:使用golang编译启动器程序成二进制文件
  • 第三步:使用一个外部class文件作为main启动入口
  • 第四步:将加密jar启动器程序成二进制文件和这个外部class文件放到一个文件夹下,一起打成可执行jar
注意:
  • 此时这个可执行jar是未加密的,通过反编译可以完全还原这个class文件,建议不要在这里写过多的东西,它只是起到一个启动作用。
  • 若能接受使用启动器程序成二进制文件启动加密的jar,只需要关心前面2步即可。后面再次打成可执行jar是为了使用jdk14以上jpackage命令

一、加密项目

  • 关于xjar工具
    我这里使用了io.xjar github地址。首先感谢作者为此做出的贡献,落地并解决了一个不算简单也不算难的问题。但出于以下原因建议自己下载此项目的源代码,自己改造:

    • 截止当前文章时间,此项目更新最后一次提交是13个月
    • 此项目的issues问题不少,但是没有一个被作者解决或回复的,问题很多,总体看起来作者不具备积极性
    • 打包后的文件过大、可能会出现一些代码兼容问题,需要自己进行优化
    • 基于开源代码加密有被破解的风险,需要自己修改一些东西来做到防止破解
    • 使用xjar2种方式,我建议直接使用它的源代码,并通过java代码调用,不通过编译插件集成,降低破解几率
    • 当前io.xjar的编译插件是可以正常引用下载的,但是jar依赖的方式会无法下载(即使你按Wiki做了配置也无济于事),所以只能下载源代码后将其deploy到自己的私服上使用(或者本地install)
    • 如时间充足,大可以参考此项目的一些实现来重新开发一个开源项目
  • 项目引用依赖(依赖下载不到的请看上一段关于xjar工具的说明)

<dependency>
            <groupId>io.xjar</groupId>
            <artifactId>xjar</artifactId>
            <version>4.0.2-SNAPSHOT</version>
</dependency>
  • 在当前项目中,新建一个类,用于加密通过正常编译得到的原始jar文件
语法自行参阅io.xjarWiki
package com.robot.main;

import io.xjar.XCryptos;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class JarEncryption {
    private static Logger logger = LoggerFactory.getLogger(JarEncryption.class);

    public static void main(String[] args) {
        logger.debug("********** 加密参数 ********** :");
        logger.debug("********** put:{}", args[0]);
        logger.debug("********** out:{}", args[1]);
        logger.debug("********** key:{}", args[2]);
        try {
            XCryptos.encryption()
                    .from(args[0]) //需要加密的jar文件
                    .use(args[2]) //使用的加密秘钥
                    .exclude("/images/**/*") //排除加密路径(此路径不加密)
                    .to(args[1]); //生成的加密jar文件存放的位置
            logger.debug("**********  加密完成 ********** ");
        } catch (Exception e) {
            logger.error("加密异常", e);
        } finally {
            System.exit(0);
        }
    }
}

  • 写一个脚本来调用加密,脚本都已经参数化,根据实际情况自行替换即可
#!/usr/bin/env bash
# !/bin/sh
set -m
# maven配置文件的位置-默认使用当前项目下的
MAVEN_CLI_OPTS="settings.xml"
# 项目可执行jar于当前脚本文件位置的相对文件夹
TARGET="robot-main/target/"
# 项目可执行jar相对位置
FAT_JAR=${TARGET}"robot-main-1.0-SNAPSHOT-fat.jar"
# 可执行jar中的加密服务类
ENCRYPTION_MAIN="com.robot.main.JarEncryption"
# 加密后的jar相对位置
ENCRYPTION_JAR=${TARGET}"robot.jar"
# 加密秘钥-默认使用当前git提交的sha值
SECRET_KEY=$(git show -s --format=%ct $CI_COMMIT_SHA)
function environment_check() {
    echo -e "\033[36m 运行环境检查: \033[0m"
    echo -e "\033[37m java 环境: \033[0m"
    java -version
    echo -e "\033[37m maven 环境: \033[0m"
    mvn -version
    echo -e "\033[37m go 环境: \033[0m"
    go version
}
function maven_package() {
    environment_check
    echo -e "\033[36m 开始编译、打包: \033[0m"
    mvn -s ${MAVEN_CLI_OPTS} clean compile package -Dmaven.test.skip=true
}
function encryption_jar() {
    echo -e "\033[36m 开始加密可执行源包: \033[0m"
    java -cp ${FAT_JAR} ${ENCRYPTION_MAIN} ${FAT_JAR} ${ENCRYPTION_JAR} ${SECRET_KEY}
}
environment_check
maven_package
encryption_jar
  • 运行结果
    JarEncryption-class调用输出
  • 这时候得到了几个文件(高亮选中的4个文件)
    JarEncryption-class调用结果
我这里的文件跟你们生成的不一样,是因为我改了源代码的文件生成名
最底下那3个jar是正常编译产生的,带fat的是我这里未加密正常启动的jar,实际上我就是对这个jar进行了加密,生成了robot.jar文件
  • 感兴趣的可以拿着加密后的文件去反编译看看得到什么,完全符合预期,无法通过反编译得到任何内容
    反编译结果
    这种方式会加密所有的内容,包括你项目中引用的jar包

二、使用go编译得到二进制启动文件

最简单的方法:直接到target下执行go build exec.go即可。
为了后续简单,我进行了脚本集成。

#!/usr/bin/env bash
# !/bin/sh
set -m
# maven配置文件的位置-默认使用当前项目下的
MAVEN_CLI_OPTS="settings.xml"
# 项目可执行jar于当前脚本文件位置的相对文件夹
TARGET="robot-main/target/"
# 项目可执行jar相对位置
FAT_JAR=${TARGET}"robot-main-1.0-SNAPSHOT-fat.jar"
# 可执行jar中的加密服务类
ENCRYPTION_MAIN="com.robot.main.JarEncryption"
# 加密后的jar相对位置
ENCRYPTION_JAR=${TARGET}"robot.jar"
# 加密秘钥-默认使用当前git提交的sha值
SECRET_KEY=$(git show -s --format=%ct $CI_COMMIT_SHA)
# 加密启动程序原始go代码文件位置
GO_EXE_FILE=${TARGET}"exec.go"
# 使用go编译后的二进制启动文件
GO_EXE=${TARGET}"exec "
# 加密jar启动的参数-2种启动方式都会应用本设置
JAVA_OPTIONS="--add-opens java.base/jdk.internal.loader=ALL-UNNAMED"
# 加密jar启动的参数-数组格式表示
ARRAY_JAVA_OPTIONS=(${JAVA_OPTIONS})
function environment_check() {
    echo -e "\033[36m 运行环境检查: \033[0m"
    echo -e "\033[37m java 环境: \033[0m"
    java -version
    echo -e "\033[37m maven 环境: \033[0m"
    mvn -version
    echo -e "\033[37m go 环境: \033[0m"
    go version
}
function maven_package() {
    environment_check
    echo -e "\033[36m 开始编译、打包: \033[0m"
    mvn -s ${MAVEN_CLI_OPTS} clean compile package -Dmaven.test.skip=true
}
function encryption_jar() {
    echo -e "\033[36m 开始加密可执行源包: \033[0m"
    java -cp ${FAT_JAR} ${ENCRYPTION_MAIN} ${FAT_JAR} ${ENCRYPTION_JAR} ${SECRET_KEY}
}
function go_build() {
    echo -e "\033[36m 开始编译二进制启动程序: \033[0m"
    go build -o ${GO_EXE} ${GO_EXE_FILE}
}
# 启动方式1-使用加密程序直接启动
function exe_go_jar() {
    echo -e "\033[36m 使用二进制文件启动: \033[0m"
    ./${GO_EXE} java ${JAVA_OPTIONS} -jar ${ENCRYPTION_JAR}
}
environment_check
maven_package
encryption_jar
go_build
exe_go_jar
  • 脚本运行结果
    go编译结果
    生成了exec可执行文件,到这里可以直接./exec robot.jar 启动加密程序了
    不要问我能不能生成exec.exe,问就是能,没试过或者完全不懂go的可以放弃

三、制作外部class启动

我想将这种./exec robot.jar启动的方式改造一下,谁没事带个二进制启动文件满街跑,太不优雅了。所以新建一个类,作为启动入口

注意:
  • 这个类不要调用任何非jdk的包,如果必须调用,建议单独弄成一个项目
  • 我在项目中建了一个exec目录,让它单独存在,因为正常编译的时候会自动生成一个对应的目录
  • 所有相关实现在当前类完成,目前这个类实现了根据参数和操作系统来组装启动命令,交给Runtime.getRuntime().exec完成启动。记得用个线程join一下,原因嘛,你可以不用试试,哈哈哈
package com.exec;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

public class ExecApp {
    final static String GLOBAL_JAVA_OPTIONS = "--add-opens java.base/jdk.internal.loader=ALL-UNNAMED";
     static String ROOT_PATH;

    public static void main(String[] args) {
        System.out.println("********** 启动参数: ********** ");
        String goExe, encryptionJar;
        int arrayJavaOptions = 0;
        if (null != args && args.length >= 3) {
            goExe = args[0];
            encryptionJar = args[1];
            arrayJavaOptions = Integer.parseInt(args[2]);
        } else {
            ROOT_PATH= Objects.requireNonNull(ExecApp.class.getClassLoader().getResource("/com/exec")).getPath();
            goExe = ROOT_PATH + "/exec";
            encryptionJar = ROOT_PATH + "/robot.jar";
        }
        System.out.println("********** GO_EXE:" + goExe);
        System.out.println("********** ENCRYPTION_JAR:" + encryptionJar);
        System.out.println("********** ARRAY_JAVA_OPTIONS个数:" + arrayJavaOptions);
        StringBuilder JAVA_OPTIONS = new StringBuilder();
        int index;
        String options;
        for (int i = 0; i < arrayJavaOptions; i++) {
            index = i + 3;
            options = args[index];
            if (i != 0) JAVA_OPTIONS.append(" ");
            JAVA_OPTIONS.append(options);
            System.out.println("**********args[" + index + "] JAVA_OPTIONS@" + i + ":" + options);
        }
        if (!JAVA_OPTIONS.toString().contains(GLOBAL_JAVA_OPTIONS)) {
            JAVA_OPTIONS.append(" ").append(GLOBAL_JAVA_OPTIONS);
        }
        System.out.println("********** JAVA_OPTIONS:" + JAVA_OPTIONS);
        String cmd;
        //todo 需要完善在windows下的nohup java启动命令
        if (!OSinfo.isWindows()) {
            cmd = "nohup " + goExe + " java " + JAVA_OPTIONS + " -jar " + encryptionJar + " &";
        } else {
            goExe = goExe + ".exe";
            cmd = goExe + " java " + JAVA_OPTIONS + " -jar " + encryptionJar + " &";
        }
        Thread thread = new Thread(() -> exeCmd(cmd, null));
        thread.start();
        System.out.println("********** 启动完成 ********** ");
        try {
            thread.join();
        } catch (InterruptedException e) {
            System.err.println("启动线程异常:" + e);
        } finally {
            System.exit(0);
        }
    }

    /**
     * 执行shell命令并且打印执行结果
     *
     * @return 按行返回执行结果
     * @throws IOException
     */
    public static List<String> exeCmd(String cmd, List<String> envp, String... args) {
        List<String> list = new ArrayList<>();
        if (null != args) cmd = cmd + " " + String.join(" ", args);
        System.out.println("cmd:" + cmd);
        int status;
        Process process;
        try {
            if (null == envp) process = Runtime.getRuntime().exec(cmd);
            else process = Runtime.getRuntime().exec(cmd, envp.toArray(new String[0]));
            status = process.waitFor();
        } catch (Exception e) {
            System.err.println("exec error:" + e);
            return list;
        }
        if (status != 0) {
            System.err.println("Failed to call shell's command =>> status:" + status);
            try (BufferedReader inputError = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
                String line = "";
                while ((line = inputError.readLine()) != null) {
                    System.err.println(line);
                }
            } catch (IOException e) {
                System.err.println("error [" + cmd + " ]shell error:" + e);
            }
        }
        try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line = "";
            while ((line = input.readLine()) != null) {
                list.add(line);
            }
        } catch (Exception e) {
            System.err.println("run [" + cmd + " ]shell error:" + e);
        }
        return list;
    }

    public static class OSinfo {
        private static String OS = System.getProperty("os.name").toLowerCase();

        public static boolean isLinux() {
            return OS.indexOf("linux") >= 0;
        }

        public static boolean isMacOS() {
            return OS.indexOf("mac") >= 0 && OS.indexOf("os") > 0 && OS.indexOf("x") < 0;
        }

        public static boolean isMacOSX() {
            return OS.indexOf("mac") >= 0 && OS.indexOf("os") > 0 && OS.indexOf("x") > 0;
        }

        public static boolean isWindows() {
            return OS.indexOf("windows") >= 0;
        }
    }
}

  • 同样写了个脚本使用class启动
#!/usr/bin/env bash
# !/bin/sh
set -m
# maven配置文件的位置-默认使用当前项目下的
MAVEN_CLI_OPTS="settings.xml"
# 项目可执行jar于当前脚本文件位置的相对文件夹
TARGET="robot-main/target/"
# 项目可执行jar相对位置
FAT_JAR=${TARGET}"robot-main-1.0-SNAPSHOT-fat.jar"
# 可执行jar中的加密服务类
ENCRYPTION_MAIN="com.robot.main.JarEncryption"
# 加密后的jar相对位置
ENCRYPTION_JAR=${TARGET}"robot.jar"
# 加密秘钥-默认使用当前git提交的sha值
SECRET_KEY=$(git show -s --format=%ct $CI_COMMIT_SHA)
# 加密启动程序原始go代码文件位置
GO_EXE_FILE=${TARGET}"exec.go"
# 使用go编译后的二进制启动文件
GO_EXE=${TARGET}"exec "
# 加密jar启动的参数-2种启动方式都会应用本设置
JAVA_OPTIONS="--add-opens java.base/jdk.internal.loader=ALL-UNNAMED"
# 加密jar启动的参数-数组格式表示
ARRAY_JAVA_OPTIONS=(${JAVA_OPTIONS})
# main-class中的启动服务类
EXEC_APP_MAIN="com.exec.ExecApp"
function environment_check() {
    echo -e "\033[36m 运行环境检查: \033[0m"
    echo -e "\033[37m java 环境: \033[0m"
    java -version
    echo -e "\033[37m maven 环境: \033[0m"
    mvn -version
    echo -e "\033[37m go 环境: \033[0m"
    go version
}
function maven_package() {
    environment_check
    echo -e "\033[36m 开始编译、打包: \033[0m"
    mvn -s ${MAVEN_CLI_OPTS} clean compile package -Dmaven.test.skip=true
}
function encryption_jar() {
    echo -e "\033[36m 开始加密可执行源包: \033[0m"
    java -cp ${FAT_JAR} ${ENCRYPTION_MAIN} ${FAT_JAR} ${ENCRYPTION_JAR} ${SECRET_KEY}
}
function go_build() {
    echo -e "\033[36m 开始编译二进制启动程序: \033[0m"
    go build -o ${GO_EXE} ${GO_EXE_FILE}
}
# 启动方式1-使用加密程序直接启动
function exe_go_jar() {
    echo -e "\033[36m 使用二进制文件启动: \033[0m"
    ./${GO_EXE} java ${JAVA_OPTIONS} -jar ${ENCRYPTION_JAR}
}
# 启动方式2-使用main-class启动加密程序
function exe_class_jar() {
    echo -e "\033[36m 使用main-class独立启动加密 jar 程序: \033[0m"
    java -classpath ${TARGET}"classes" ${EXEC_APP_MAIN} ${GO_EXE} ${ENCRYPTION_JAR} ${#ARRAY_JAVA_OPTIONS[@]} ${ARRAY_JAVA_OPTIONS[*]}
}
environment_check
maven_package
encryption_jar
go_build
exe_go_jar

这个脚本已经完成了class启动,接下来打成一个新的可执行jar

  • 转移资源,准备打成jar
    • target/exectarget/robot.jar转移到对应启动class文件转移启动资源

四、将新的class启动文件和相关资源打成可运行jar

目前已经有了以下资源:

  • 编译后的ExecApp.class
  • 二进制启动文件exec
  • 加密的robot.jar
    这些资源全部放到target/classes/com/exec下,还缺少一个打包清单,以免丢失了main-class,造成打成的包无法找到启动函数的问题。
  • 新建一个MANIFEST.MF文件
    Main-Class的值根据自己的启动类情况修改即可
Manifest-Version: 1.0
Created-By: 11.0.8 (AdoptOpenJDK)
Main-Class: com.exec.ExecApp
  • 打包成可执行jar并且启动的脚本
#!/usr/bin/env bash
# !/bin/sh
set -m
# maven配置文件的位置-默认使用当前项目下的
MAVEN_CLI_OPTS="settings.xml"
# 项目可执行jar于当前脚本文件位置的相对文件夹
TARGET="robot-main/target/"
# 项目可执行jar相对位置
FAT_JAR=${TARGET}"robot-main-1.0-SNAPSHOT-fat.jar"
# 可执行jar中的加密服务类
ENCRYPTION_MAIN="com.robot.main.JarEncryption"
# 加密后的jar相对位置
ENCRYPTION_JAR=${TARGET}"robot.jar"
# 加密秘钥-默认使用当前git提交的sha值
SECRET_KEY=$(git show -s --format=%ct $CI_COMMIT_SHA)
# 加密启动程序原始go代码文件位置
GO_EXE_FILE=${TARGET}"exec.go"
# 使用go编译后的二进制启动文件
GO_EXE=${TARGET}"exec "
# 加密jar启动的参数-2种启动方式都会应用本设置
JAVA_OPTIONS="--add-opens java.base/jdk.internal.loader=ALL-UNNAMED"
# 加密jar启动的参数-数组格式表示
ARRAY_JAVA_OPTIONS=(${JAVA_OPTIONS})
# main-class中的启动服务类
EXEC_APP_MAIN="com.exec.ExecApp"
# 平台打包资源文件夹
PLATFORM_OUT=${TARGET}"out/"
function environment_check() {
    echo -e "\033[36m 运行环境检查: \033[0m"
    echo -e "\033[37m java 环境: \033[0m"
    java -version
    echo -e "\033[37m maven 环境: \033[0m"
    mvn -version
    echo -e "\033[37m go 环境: \033[0m"
    go version
}
function maven_package() {
    environment_check
    echo -e "\033[36m 开始编译、打包: \033[0m"
    mvn -s ${MAVEN_CLI_OPTS} clean compile package -Dmaven.test.skip=true
}
function encryption_jar() {
    echo -e "\033[36m 开始加密可执行源包: \033[0m"
    java -cp ${FAT_JAR} ${ENCRYPTION_MAIN} ${FAT_JAR} ${ENCRYPTION_JAR} ${SECRET_KEY}
}
function go_build() {
    echo -e "\033[36m 开始编译二进制启动程序: \033[0m"
    go build -o ${GO_EXE} ${GO_EXE_FILE}
}
# 启动方式1-使用加密程序直接启动
function exe_go_jar() {
    echo -e "\033[36m 使用二进制文件启动: \033[0m"
    ./${GO_EXE} java ${JAVA_OPTIONS} -jar ${ENCRYPTION_JAR}
}
# 启动方式2-使用main-class启动加密程序
function exe_class_jar() {
    echo -e "\033[36m 使用main-class独立启动加密 jar 程序: \033[0m"
    java -classpath ${TARGET}"classes" ${EXEC_APP_MAIN} ${GO_EXE} ${ENCRYPTION_JAR} ${#ARRAY_JAVA_OPTIONS[@]} ${ARRAY_JAVA_OPTIONS[*]}
}
# 启动方式3-使用execjar启动加密程序
function exe_jar() {
    echo -e "\033[36m 使用main-class独立启动加密 jar 程序: \033[0m"
    java -jar ${PLATFORM_OUT}${EXECAPP_JAR}
}
function exe_package() {
    echo -e "\033[36m exejar打包====> 1:复制资源文件到 ${EXECAPP_JAR_OUT} 目录 : \033[0m"
    mkdir ${PLATFORM_OUT}
    mv ${GO_EXE} ${EXECAPP_JAR_OUT}
    mv ${ENCRYPTION_JAR} ${EXECAPP_JAR_OUT}
    mv ${EXECAPP_JAR_MF} ${EXECAPP_JAR_OUT}
    echo -e "\033[36m exejar打包====> 2:打成启动jar包 : \033[0m"
    cd ${TARGET}"classes"
    jar cfmv ../"out/"${EXECAPP_JAR} ${EXECAPP_JAR_MF} com/exec
    cd ${WORK}
}
environment_check
maven_package
encryption_jar
go_build
exe_go_jar
exe_package
exe_jar
  • 脚本执行
    class打成启动jar

注意:

到这里已经得到一个正常的可执行jar了,下面是使用jdk14及以上的特性将可执行jar打包成不同平台的应用程序的方法

五、利用jpackage进行平台分发打包

最简单的办法,直接运行jpackage指令进行打包即可
当然我还是会做成脚本化的,并且提供参数定制的功能

  • 新建一个类用来提供jpackage打包
package com.robot.main;

import com.robot.common.util.ShellUtils;
import com.robot.common.util.osinfo.OSinfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PlatformPackage {
    private static Logger logger = LoggerFactory.getLogger(PlatformPackage.class);
    static String CMD = "jpackage --input ${input} --main-class ${main-class} --main-jar ${main-jar} --name ${name} --type ${type} --app-version ${app-version} --description RPA --vendor ayouran.Inc --verbose";

    public static void main(String[] args) {
        logger.debug("********** 平台分发参数: ********** ");
        logger.debug("********** os:{}", OSinfo.getOSname());
        logger.debug("********** input:{}", args[0]);
        logger.debug("********** main-class:{}", args[1]);
        logger.debug("********** main-jar:{}", args[2]);
        logger.debug("********** name:{}", args[3]);
        logger.debug("********** app-version:{}", args[4]);
        CMD = CMD.replace("${input}", args[0]);
        CMD = CMD.replace("${main-class}", args[1]);
        CMD = CMD.replace("${main-jar}", args[2]);
        CMD = CMD.replace("${name}", args[3]);
        CMD = CMD.replace("${app-version}", args[4]);
        if (OSinfo.isWindows()) {
            CMD = CMD.replace("${type}", "app-image");
        } else if (OSinfo.isMacOS() || OSinfo.isMacOSX()) {
            CMD = CMD.replace("${type}", "dmg");
        } else {
            logger.debug("暂未实现");
        }
        Thread thread = new Thread(() -> ShellUtils.exeCmd(CMD, null));
        thread.start();
        logger.debug("********** 平台分发完成 ********** ");
        try {
            thread.join();
        } catch (InterruptedException e) {
            logger.error("平台分发线程异常", e);
        } finally {
            System.exit(0);
        }
    }
}

  • 打包脚本
# maven配置文件的位置-默认使用当前项目下的
MAVEN_CLI_OPTS="settings.xml"
# 项目可执行jar于当前脚本文件位置的相对文件夹
TARGET="robot-main/e/"
# 项目可执行jar相对位置
FAT_JAR=${TARGET}"robot-main-1.0-SNAPSHOT-fat.jar"
# 可执行jar中的加密服务类
ENCRYPTION_MAIN="com.robot.main.JarEncryption"
# 可执行jar中的平台分发服务类
PLATFORM_PACKAGE_MAIN="com.robot.main.PlatformPackage"
# 平台打包资源文件夹
PLATFORM_OUT=${TARGET}"out/"
# main-class中的启动服务类
EXEC_APP_MAIN="com.exec.ExecApp"
# 启动器jar
EXECAPP_JAR="ExecApp.jar"
# 启动器jar打包目录
EXECAPP_JAR_OUT=${TARGET}"classes/com/exec"
# 启动器jar打包清单
EXECAPP_JAR_MF="MANIFEST.MF"
# 加密后的jar相对位置
ENCRYPTION_JAR=${TARGET}"robot.jar"
# 加密启动程序原始go代码文件位置
GO_EXE_FILE=${TARGET}"exec.go"
# 使用go编译后的二进制启动文件
GO_EXE=${TARGET}"exec "
# 加密jar启动的参数-2种启动方式都会应用本设置
JAVA_OPTIONS="--add-opens java.base/jdk.internal.loader=ALL-UNNAMED"
# 加密jar启动的参数-数组格式表示
ARRAY_JAVA_OPTIONS=(${JAVA_OPTIONS})
# 加密秘钥-默认使用当前git提交的sha值
SECRET_KEY=$(git show -s --format=%ct $CI_COMMIT_SHA)
# 当前工作目录
WORK=$(pwd)
# 检测到的java进程pid
psid=0
function environment_check() {
    echo -e "\033[36m 运行环境检查: \033[0m"
    echo -e "\033[37m java 环境: \033[0m"
    java -version
    echo -e "\033[37m maven 环境: \033[0m"
    mvn -version
    echo -e "\033[37m go 环境: \033[0m"
    go version
}
function maven_package() {
    environment_check
    echo -e "\033[36m 开始编译、打包: \033[0m"
    mvn -s ${MAVEN_CLI_OPTS} clean compile package -Dmaven.test.skip=true
}
function encryption_jar() {
    echo -e "\033[36m 开始加密可执行源包: \033[0m"
    java -cp ${FAT_JAR} ${ENCRYPTION_MAIN} ${FAT_JAR} ${ENCRYPTION_JAR} ${SECRET_KEY}
}
function go_build() {
    echo -e "\033[36m 开始编译二进制启动程序: \033[0m"
    go build -o ${GO_EXE} ${GO_EXE_FILE}
}
# 启动方式1-使用加密程序直接启动
function exe_go_jar() {
    echo -e "\033[36m 使用二进制文件启动: \033[0m"
    ./${GO_EXE} java ${JAVA_OPTIONS} -jar ${ENCRYPTION_JAR}
}
# 启动方式2-使用main-class启动加密程序
function exe_class_jar() {
    echo -e "\033[36m 使用main-class独立启动加密 jar 程序: \033[0m"
    java -classpath ${TARGET}"classes" ${EXEC_APP_MAIN} ${GO_EXE} ${ENCRYPTION_JAR} ${#ARRAY_JAVA_OPTIONS[@]} ${ARRAY_JAVA_OPTIONS[*]}
}
# 启动方式3-使用execjar启动加密程序
function exe_jar() {
    echo -e "\033[36m 使用main-class独立启动加密 jar 程序: \033[0m"
    java -jar ${PLATFORM_OUT}${EXECAPP_JAR}
}

function exe_package() {
    echo -e "\033[36m exejar打包====> 1:复制资源文件到 ${EXECAPP_JAR_OUT} 目录 : \033[0m"
    mkdir ${PLATFORM_OUT}
    mv ${GO_EXE} ${EXECAPP_JAR_OUT}
    mv ${ENCRYPTION_JAR} ${EXECAPP_JAR_OUT}
    mv ${EXECAPP_JAR_MF} ${EXECAPP_JAR_OUT}
    echo -e "\033[36m exejar打包====> 2:打成启动jar包 : \033[0m"
    cd ${TARGET}"classes"
    jar cfmv ../"out/"${EXECAPP_JAR} ${EXECAPP_JAR_MF} com/exec
    cd ${WORK}
}
function platform_package() {
    echo -e "\033[36m 准备平台分发====> 3:平台分发 ${WORK} 目录: \033[0m"
    java -cp ${FAT_JAR} ${PLATFORM_PACKAGE_MAIN} ${PLATFORM_OUT} ${EXEC_APP_MAIN} "ExecApp.jar" "robot" "1.0.0"
}
environment_check
maven_package
encryption_jar
go_build
exe_go_jar
exe_package
exe_jar
platform_package

执行它,最终可以在当前项目工作区得到一个当前操作系统对应的标准应用程序包
平台分发程序包

六、资源说明

  • 完整的脚本
#!/usr/bin/env bash
# !/bin/sh
set -m
# 启动菜单
menu_key=$1
# maven配置文件的位置-默认使用当前项目下的
MAVEN_CLI_OPTS="settings.xml"
# 项目可执行jar于当前脚本文件位置的相对文件夹
TARGET="robot-main/e/"
# 项目可执行jar相对位置
FAT_JAR=${TARGET}"robot-main-1.0-SNAPSHOT-fat.jar"
# 可执行jar中的加密服务类
ENCRYPTION_MAIN="com.robot.main.JarEncryption"
# 可执行jar中的平台分发服务类
PLATFORM_PACKAGE_MAIN="com.robot.main.PlatformPackage"
# 平台打包资源文件夹
PLATFORM_OUT=${TARGET}"out/"
# main-class中的启动服务类
EXEC_APP_MAIN="com.exec.ExecApp"
# 启动器jar
EXECAPP_JAR="ExecApp.jar"
# 启动器jar打包目录
EXECAPP_JAR_OUT=${TARGET}"classes/com/exec"
# 启动器jar打包清单
EXECAPP_JAR_MF="MANIFEST.MF"
# 加密后的jar相对位置
ENCRYPTION_JAR=${TARGET}"robot.jar"
# 加密启动程序原始go代码文件位置
GO_EXE_FILE=${TARGET}"exec.go"
# 使用go编译后的二进制启动文件
GO_EXE=${TARGET}"exec "
# 加密jar启动的参数-2种启动方式都会应用本设置
JAVA_OPTIONS="--add-opens java.base/jdk.internal.loader=ALL-UNNAMED"
# 加密jar启动的参数-数组格式表示
ARRAY_JAVA_OPTIONS=(${JAVA_OPTIONS})
# 加密秘钥-默认使用当前git提交的sha值
SECRET_KEY=$(git show -s --format=%ct $CI_COMMIT_SHA)
# 当前工作目录
WORK=$(pwd)
# 检测到的java进程pid
psid=0
function environment_check() {
    echo -e "\033[36m 运行环境检查: \033[0m"
    echo -e "\033[37m java 环境: \033[0m"
    java -version
    echo -e "\033[37m maven 环境: \033[0m"
    mvn -version
    echo -e "\033[37m go 环境: \033[0m"
    go version
}
function maven_package() {
    environment_check
    echo -e "\033[36m 开始编译、打包: \033[0m"
    mvn -s ${MAVEN_CLI_OPTS} clean compile package -Dmaven.test.skip=true
}
function encryption_jar() {
    echo -e "\033[36m 开始加密可执行源包: \033[0m"
    java -cp ${FAT_JAR} ${ENCRYPTION_MAIN} ${FAT_JAR} ${ENCRYPTION_JAR} ${SECRET_KEY}
}
function go_build() {
    echo -e "\033[36m 开始编译二进制启动程序: \033[0m"
    go build -o ${GO_EXE} ${GO_EXE_FILE}
}
# 启动方式1-使用加密程序直接启动
function exe_go_jar() {
    echo -e "\033[36m 使用二进制文件启动: \033[0m"
    ./${GO_EXE} java ${JAVA_OPTIONS} -jar ${ENCRYPTION_JAR}
}
# 启动方式2-使用main-class启动加密程序
function exe_class_jar() {
    echo -e "\033[36m 使用main-class独立启动加密 jar 程序: \033[0m"
    java -classpath ${TARGET}"classes" ${EXEC_APP_MAIN} ${GO_EXE} ${ENCRYPTION_JAR} ${#ARRAY_JAVA_OPTIONS[@]} ${ARRAY_JAVA_OPTIONS[*]}
}
# 启动方式3-使用execjar启动加密程序
function exe_jar() {
    echo -e "\033[36m 使用main-class独立启动加密 jar 程序: \033[0m"
    java -jar ${PLATFORM_OUT}${EXECAPP_JAR}
}

function exe_package() {
    echo -e "\033[36m exejar打包====> 1:复制资源文件到 ${EXECAPP_JAR_OUT} 目录 : \033[0m"
    mkdir ${PLATFORM_OUT}
    mv ${GO_EXE} ${EXECAPP_JAR_OUT}
    mv ${ENCRYPTION_JAR} ${EXECAPP_JAR_OUT}
    mv ${EXECAPP_JAR_MF} ${EXECAPP_JAR_OUT}
    echo -e "\033[36m exejar打包====> 2:打成启动jar包 : \033[0m"
    cd ${TARGET}"classes"
    jar cfmv ../"out/"${EXECAPP_JAR} ${EXECAPP_JAR_MF} com/exec
    cd ${WORK}
}
function platform_package() {
    echo -e "\033[36m 准备平台分发====> 3:平台分发 ${WORK} 目录: \033[0m"
    java -cp ${FAT_JAR} ${PLATFORM_PACKAGE_MAIN} ${PLATFORM_OUT} ${EXEC_APP_MAIN} "ExecApp.jar" "robot" "1.0.0"
}
function jar_kill() {
    initPsid
    if [[ ${psid} -ne 0 ]]; then
        kill -s 9 ${psid}
        echo -e "\033[33m 已杀死:${psid} \033[0m"
    fi
}
function initPsid(){
    echo -e "\033[36m java进程检查:${FAT_JAR}: \033[0m"
    javaps=`jps -l | grep "${FAT_JAR}"`
    if [[ -n "$javaps" ]]; then
        psid=`echo ${javaps} | awk '{ print $1}'`
    else
        psid=0
    fi
}
function main(){
    case $menu_key in
          -package | p | -p)
                   echo -e "\033[34m 当前模式 package: \033[0m"
                   maven_package
               ;;
          -encryption | e | -e)
                   echo -e "\033[34m 当前模式 encryption: \033[0m"
                   maven_package
                   encryption_jar
               ;;
          -run1 | r1 | -r1)
                   echo -e "\033[34m 当前模式 使用二进制加密启动程序: \033[0m"
                   jar_kill
                   maven_package
                   encryption_jar
                   go_build
                   jar_kill
                   exe_go_jar
               ;;
          -run2 | r2 | -r2)
                   echo -e "\033[34m 当前模式 使用 exec-main-class 启动加密程序: \033[0m"
                   jar_kill
                   maven_package
                   encryption_jar
                   go_build
                   jar_kill
                   exe_class_jar
               ;;
          -run3 | r3 | -r3)
                   echo -e "\033[34m 当前模式 使用 exec-jar 启动加密程序: \033[0m"
                   jar_kill
                   maven_package
                   encryption_jar
                   go_build
                   jar_kill
                   exe_package
                   exe_jar
               ;;
          -platform | pf | -pf)
                   echo -e "\033[34m 当前模式 平台分发可执行包: \033[0m"
                   jar_kill
                   maven_package
                   encryption_jar
                   go_build
                   jar_kill
                   exe_package
                   platform_package
              ;;
          *)
                echo -e "\033[41;33m 此脚本用于在 Liunx 上编译、打包、加密、平台分发\033[0m"
                echo -e "\033[33m 注意: \033[0m"
                echo -e "\033[35m     1、此脚本的运行需要当前设备具有以下环境 \033[0m \033[33m (会以黄色字提醒)\033[0m"
                echo -e "\033[33m          openjdk11 、maven3.x 、go 1.1x\033[0m"
                echo -e "\033[35m     2、此脚本有5种运行模式,见使用帮助\033[0m"
                echo -e "\033[34m 使用帮助: \033[0m"
                echo -e "\033[35m     -package | p | -p : 在当前系统上使用 maven 进行编译\033[0m"
                echo -e "\033[35m     -encryption | e | -e : 加密 package 产生的可运行 jar 包\033[0m"
                echo -e "\033[35m     -run1 | r1 | -r1 : 使用二进制文件启动程序直接运行加密后的 jar 程序包\033[0m"
                echo -e "\033[35m     -run2 | r2 | -r2 : 使用 exec-main-class 运行加密后的 jar 程序包\033[0m"
                echo -e "\033[35m     -run3 | r3 | -r3 : 使用 exec-jar 运行加密后的 jar 程序包\033[0m"
                echo -e "\033[35m     -platform | pf | -pf : 平台分发可执程序包\033[0m"
           ;;
     esac
}
main
  • 脚本功能说明
    脚本功能说明
  • io.xjar源代码中最基本的更改点
    • 为了解包之后不直接根据名称就猜到你的加密手段
      更改java代码模板
    • 关闭jvm功能-必须加在这里,因为外部的启动class文件是不安全的,随时可能被更改加入新的参数(也可以在这里进行参数排除)
      更改go源代码

七、总结

  • 这个方式会把一个可执行jar做成黑盒,只能查询java进程了杀死,或者使用它提供的标准接口提供服务,不能做别的操作。当然可以自行在jar中加入优雅停机功能

  • 打出来的加密包过大,在性能上还未进行测试,暂时没有评估带来的影响

  • 通过加密及打包会发生一些相对文件路径的问题

  • 平台分发出的包只能对当前平台有效,有效测试在win11下打包可以在win7及以上系统上使用,而mac osx并不通用,哪怕是系统小版本不同也无法使用

  • 启动程序后,在主动关闭程序时有时会发生程序被系统挂起的问题,原因可能是资源释放的问题

    个人公众号

0

评论区