前言
通常来说,将代码打成jar包后,可以通过反编译工具得到源码。这对于代码隐蔽性是极大的挑战。常用的加密方式有代码混淆、编译加密运行动态解密、魔改JVM等;
本文采用第二种方式;
编译期加密
前言
首先要知道,代码变为jar包需要经过编译的;maven将编译后的class文件统一打成jar包。但是这样,就等于泄露了代码源文件。所以需要在maven编译时做出调整,将指定的class替换成加密后的class,然后再打进jar包;
maven插件
通过自定义maven插件进行的实现
首先我们来看pom.xml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <groupId>top.bubuzi</groupId> <artifactId>encryption-maven-plugin</artifactId>
<packaging>maven-plugin</packaging> <version>bubuzi.changsha.RS1</version>
<dependencies> <dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-plugin-api</artifactId> <version>3.8.1</version> </dependency> </dependencies>
|
编写插件加密类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
| package top.bubuzi;
import top.bubuzi.SafeLoaderUtil; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException;
import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List;
public class FramePlugin extends AbstractMojo {
private List<String> protectClassNames;
private List<String> noCompileClassNames;
private String root; private File output; private List<String> protectClassNameList = new ArrayList<>();
@Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().info("执行jar包加密操作"); this.output = new File(root); try { this.protectCore(this.output); } catch (IOException e) { e.printStackTrace(); } }
private void protectCore(File root) throws IOException { if (root.isDirectory()) { for (File file : root.listFiles()) { protectCore(file); } } if (root.getName().endsWith(".class")) { String className = root.getName().replace(".class", ""); boolean flag = false; if (protectClassNames != null && protectClassNames.size() > 0) { for (String item : protectClassNames) { if (className.equals(item)) { flag = true; } } } if (noCompileClassNames.contains(className)) { boolean deleteResult = root.delete(); if (!deleteResult) { System.gc(); deleteResult = root.delete(); } getLog().info(String.format("[noCompile-deleteResult]:%s", deleteResult)); } if (flag && !protectClassNameList.contains(className)) { protectClassNameList.add(className); getLog().info(String.format("[protectCore]:%s", className)); FileOutputStream fos = null; try { final byte[] instrumentBytes = doProtectCore(root); String folderPath = output.getAbsolutePath() + "\\" + "classes"; File folder = new File(folderPath); if (!folder.exists()) { folder.mkdir(); } folderPath = output.getAbsolutePath() + "\\" + "classes" + "\\" + "coreclass"; folder = new File(folderPath); if (!folder.exists()) { folder.mkdir(); } String filePath = output.getAbsolutePath() + "\\" + "classes" + "\\" + "coreclass" + "\\" + className + ".class"; getLog().info("[filePath]: " + filePath); File protectFile = new File(filePath); if (protectFile.exists()) { protectFile.delete(); } protectFile.createNewFile(); fos = new FileOutputStream(protectFile); fos.write(instrumentBytes); fos.flush(); } catch (Exception e) { getLog().info("[protectCore-exception]:" + className); e.printStackTrace(); } finally { if (fos != null) { fos.close(); } if (root.exists()) { boolean deleteResult = root.delete(); if (!deleteResult) { System.gc(); deleteResult = root.delete(); } getLog().info("[protectCore-deleteResult]:" + deleteResult); } } } } }
private byte[] doProtectCore(File clsFile) throws MojoExecutionException { try { FileInputStream inputStream = new FileInputStream(clsFile); byte[] bytes = new byte[(int) clsFile.length()]; getLog().info("origin length:" + bytes.length); inputStream.read(bytes);
byte[] encipherByte = SafeLoaderUtil.encryption(bytes); inputStream.close(); return encipherByte; } catch (Exception e) { throw new MojoExecutionException("doProtectCore error", e); } }
}
|
代码种的SafeLoaderUtil.encryption的JNI调用方法可参考上一篇文章
maven插件打包安装
一定要安装至本地!!!
一定要安装至本地!!!
一定要安装至本地!!!

加密项目配置
pom.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <build> <plugins> <plugin> <groupId>top.bubuzi</groupId> <artifactId>encryption-maven-plugin</artifactId> <version>bubuzi.changsha.RS1</version> <executions> <execution> <phase>compile</phase> <goals> <goal>deepcompile</goal> </goals> </execution> </executions> <configuration> <protectClassNames> <protectClassName>LoginServiceImpl</protectClassName> </protectClassNames> </configuration> </plugin> </plugins> </build>
|
打包
配置好pom后,就可以愉快的打包了
可以看见,日志中正确输出了打包相关内容。

项目结构里,LoginServiceImpl类应该存在于impl包下

查看jar包内结构
使用压缩包的模式打开jar




可以看见,本应在impl包下的LoginServiceImpl类,去了coreclass文件夹下
这就是加密完成
通过idea反编译加密class文件,可以看见根本无法查阅

解密
运行时解密
在这里我查阅了许多资料,始终没能找到使我满意的解答方式;
这与springboot的加载机制有关系,起初我想用classloader的方式动态加载,一直未能成功,感兴趣的同志可以去了解以下springboot的加载机制,还有要注意jdk版本问题,jdk8及以下与jdk8以上的类加载机制有部分变化;
最终采用的一种折中方案;
我们都知道,spring会将beanClass封装成一个BeanDefinition对象,然后再通过bean别名,一一对应的方式注册进IOC容器;
所以,我们只需要在spring容器启动后,手动将加密类注册进容器,后续bean创建时,就会使用我们注入的class;
解密类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119
| public class StaticEncryptLoader extends ClassLoader { private static StaticEncryptLoader INSTANCE;
@Setter private DefaultListableBeanFactory beanFactory; private static final Map<String, Class> classes = new ConcurrentHashMap();
static { System.loadLibrary("EncryptionLoader"); }
private StaticEncryptLoader(ClassLoader parent){ super(parent); }
public static StaticEncryptLoader getInstance(ClassLoader parent){ if(INSTANCE == null){ synchronized (StaticEncryptLoader.class){ if(INSTANCE == null){ INSTANCE = new StaticEncryptLoader(parent); } } } return INSTANCE; } public static StaticEncryptLoader getInstance() { return INSTANCE; }
public void start(){ try { loadClasses2Cache();
} catch (IOException e) { e.printStackTrace(); } }
public void loadClasses2Cache() throws IOException { Resource[] resources = new PathMatchingResourcePatternResolver().getResources("/coreclass/**/*.class"); for(Resource resource : resources) { byte pack[] = null; InputStream inputStream = resource.getInputStream(); int count = 0; while(count == 0){ count = inputStream.available(); } pack = new byte[count]; int readCount = 0; while (readCount < count) { readCount += inputStream.read(pack, readCount, count - readCount); } byte[] decryption = SafeLoaderUtil.decryption(pack); Class<?> aClass = null; try { aClass = defineClass("top.bubuzi.service.impl.LoginServiceImpl", decryption, 0, decryption.length); } catch (ClassFormatError classFormatError) { classFormatError.printStackTrace(); } classes.put("top.bubuzi.service.impl.LoginServiceImpl",aClass);
} }
public void registryBeans() { if (!CollectionUtils.isEmpty(classes) && beanFactory != null) { classes.forEach( (k, v) -> { registryBean(k); } ); } }
private void registryBean(String className) { Class cla = classes.get(className.replace(".class", "").replaceAll("/", ".")); if (SpringUtil.isSpringBeanClass(cla) && beanFactory != null) { BeanDefinition beanDefinition = SpringUtil.buildBeanDefinition(cla); String beanName = StringUtils.uncapitalize(className); beanName = beanName.substring(beanName.lastIndexOf(".") + 1); beanName = StringUtils.uncapitalize(beanName); safeRegistry(className, beanDefinition); safeRegistry(beanName, beanDefinition); if(beanName.endsWith("Impl") ){ safeRegistry(beanName.replace("Impl",""), beanDefinition); } } } private void safeRegistry(String name,BeanDefinition beanDefinition){ if(!beanFactory.containsBean(name)){ beanFactory.registerBeanDefinition(name,beanDefinition); }else{ Object bean = beanFactory.getBean(name); System.out.println(name); } } }
|
容器加载类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Component public class SpringRegistry implements BeanFactoryPostProcessor { @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { StaticEncryptLoader instance = StaticEncryptLoader.getInstance(); if(instance==null){ return; } instance.setBeanFactory((DefaultListableBeanFactory) beanFactory); if(!(beanFactory.getBeanClassLoader() instanceof StaticEncryptLoader)){ beanFactory.setBeanClassLoader(instance); } instance.registryBeans(); } }
|
启动类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @SpringBootApplication(scanBasePackages = "top.bubuzi") @MapperScan("top.bubuzi.mapper") public class PlatformApplication {
static { StaticEncryptLoader instance = StaticEncryptLoader.getInstance(PlatformApplication.class.getClassLoader()); instance.start(); }
public static void main(String[] args) { SpringApplication.run(PlatformApplication.class, args); } }
|
启动后正常无报错即加密成功
后文
本方法只能避免jar包直接被反编译,不能实现完全的加密防护,实际上百分百防护是做不到的;
但是这里依旧有优化项点:
* 不采用beanFactory,而采用classLoader的方式
* 加密classLoader,隐藏解密程序,classLoader采用分片注入程序的方式
* 核心代码用安全性高的语言实现,如c++实现后jni调用
* 魔改JVM加载机制
希望本文能帮到你