SpringBoot詳細講解通過自定義classloader加密保護class文件
背景
最近針對公司框架進行關鍵業(yè)務代碼進行加密處理,防止通過jd-gui等反編譯工具能夠輕松還原工程代碼,相關混淆方案配置使用比較復雜且針對springboot項目問題較多,所以針對class文件加密再通過自定義的classloder進行解密加載,此方案并不是絕對安全,只是加大反編譯的困難程度,防君子不防小人,整體加密保護流程圖如下圖所示
maven插件加密
使用自定義maven插件對編譯后指定的class文件進行加密,加密后的class文件拷貝到指定路徑,這里是保存到resource/coreclass下,刪除源class文件,加密使用的是簡單的DES對稱加密
@Parameter(name = "protectClassNames", defaultValue = "") private List<String> protectClassNames; @Parameter(name = "noCompileClassNames", defaultValue = "") private List<String> noCompileClassNames; private List<String> protectClassNameList = new ArrayList<>(); private void protectCore(File root) throws IOException { if (root.isDirectory()) { for (File file : root.listFiles()) { protectCore(file); } } String className = root.getName().replace(".class", ""); if (root.getName().endsWith(".class")) { //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(); } System.out.println("【noCompile-deleteResult】:" + deleteResult); } if (flag && !protectClassNameList.contains(className)) { protectClassNameList.add(className); System.out.println("【protectCore】:" + className); FileOutputStream fos = null; try { final byte[] instrumentBytes = doProtectCore(root); //加密后的class文件保存路徑 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"; System.out.println("【filePath】:" + filePath); File protectFile = new File(filePath); if (protectFile.exists()) { protectFile.delete(); } protectFile.createNewFile(); fos = new FileOutputStream(protectFile); fos.write(instrumentBytes); fos.flush(); } catch (MojoExecutionException e) { System.out.println("【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(); } System.out.println("【protectCore-deleteResult】:" + deleteResult); } } } } } private byte[] doProtectCore(File clsFile) throws MojoExecutionException { try { FileInputStream inputStream = new FileInputStream(clsFile); byte[] content = ProtectUtil.encrypt(inputStream); inputStream.close(); return content; } catch (Exception e) { throw new MojoExecutionException("doProtectCore error", e); } }
注意事項
1.加密后的文件也是class文件,為了防止在遞歸查找中重復加密,需要對已經(jīng)加密后的class名稱記錄防止重復
2.在刪除源文件時可能出現(xiàn)編譯占用的情況,執(zhí)行System.gc()后方可刪除
3.針對自定義插件的列表形式的configuration節(jié)點可以使用List來映射
插件使用配置如圖所示
自定義classloader
創(chuàng)建CustomClassLoader繼承自ClassLoader,重寫findClass方法只處理裝載加密后的class文件,其他class交有默認加載器處理,需要注意的是默認處理不能調(diào)用super.finclass方法,在idea調(diào)試沒問題,打成jar包運行就會報加密的class中的依賴class無法加載(ClassNoDefException/ClassNotFoundException),這里使用的是當前線程的上下文的類加載器就沒有問題(Thread.currentThread().getContextClassLoader())
public class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class<?> clz = findLoadedClass(name); //先查詢有沒有加載過這個類。如果已經(jīng)加載,則直接返回加載好的類。如果沒有,則加載新的類。 if (clz != null) { return clz; } String[] classNameList = name.split("\\."); String classFileName = classNameList[classNameList.length - 1]; if (classFileName.endsWith("MethodAccess") || !classFileName.endsWith("CoreUtil")) { return Thread.currentThread().getContextClassLoader().loadClass(name); } ClassLoader parent = this.getParent(); try { //委派給父類加載 clz = parent.loadClass(name); } catch (Exception e) { //log.warn("parent load class fail:"+ e.getMessage(),e); } if (clz != null) { return clz; } else { byte[] classData = null; ClassPathResource classPathResource = new ClassPathResource("coreclass/" + classFileName + ".class"); InputStream is = null; try { is = classPathResource.getInputStream(); classData = DESEncryptUtil.decryptFromByteV2(FileUtil.convertStreamToByte(is), "xxxxxxx"); } catch (Exception e) { e.printStackTrace(); throw new ProtectClassLoadException("getClassData error"); } finally { try { if (is != null) { is.close(); } } catch (IOException e) { e.printStackTrace(); } } if (classData == null) { throw new ClassNotFoundException(); } else { clz = defineClass(name, classData, 0, classData.length); } return clz; } } }
隱藏classloader
classloader加密class文件處理方案的漏洞在于自定義類加載器是完全暴露的,只需進行分析解密流程就能獲取到原始class文件,所以我們需要對classloder的內(nèi)容進行隱藏
1.把classloader的源文件在編譯期間進行刪除(maven自定義插件實現(xiàn))
2.將classloder的內(nèi)容進行base64編碼后拆分內(nèi)容尋找多個系統(tǒng)啟動注入點寫入到loader.key文件中(拆分時寫入的路徑和文件名需要進行base64加密避免全局搜索),例如
private static void init() { String source = "dCA9IG5hbWUuc3BsaXQoIlxcLiIpOwogICAgICAgIFN0cmluZyBjbGFzc0ZpbGVOYW1lID0gY2xhc3NOYW1lTGlzdFtjbGFzc05hbWVMaXN0Lmxlbmd0aCAtIDFdOwogICAgICAgIGlmIChjbGFzc0ZpbGVOYW1lLmVuZHNXaXRoKCJNZXRob2RBY2Nlc3MiKSB8fCAhY2xhc3NGaWxlTmFtZS5lbmRzV2l0aCgiQ29yZVV0aWwiKSkgewogICAgICAgICAgICByZXR1cm4gVGhyZWFkLmN1cnJlbnRUaHJlYWQoKS5nZXRDb250ZXh0Q2xhc3NMb2FkZXIoKS5sb2FkQ2xhc3MobmFtZSk7CiAgICAgICAgfQogICAgICAgIENsYXNzTG9hZGVyIHBhcmVudCA9IHRoaXMuZ2V0UGFyZW50KCk7CiAgICAgICAgdHJ5IHsKICAgICAgICAgICAgLy/lp5TmtL7nu5nniLbnsbvliqDovb0KICAgICAgICAgICAgY2x6ID0gcGFyZW50LmxvYWRDbGFzcyhuYW1lKTsKICAgICAgICB9IGNhdGNoIChFeGNlcHRpb24gZSkgewogICAgICAgICAgICAvL2xvZy53YXJuKCJwYXJlbnQgbG9hZCBjbGFzcyBmYWls77yaIisgZS5nZXRNZXNzYWdlKCksZSk7CiAgICAgICAgfQogICAgICAgIGlmIChjbHogIT0gbnVsbCkgewogICAgICAgICAgICByZXR1cm4gY2x6OwogICAgICAgIH0gZWxzZSB7CiAgICAgICAgICAgIGJ5dGVbXSBjbGFzc0RhdGEgPSBudWxsOwogICAgICAgICAgICBDbGFzc1BhdGhSZXNvdXJjZSBjbGFzc1BhdGhSZXNvdXJjZSA9IG5ldyBDbGFzc1BhdGhSZXNvdXJjZSgiY29yZWNsYXNzLyIgKyBjbGFzc0ZpbGVOYW1lICsgIi5jbGFzcyIpOwogICAgICAgICAgICBJbnB1dFN0cmVhbSBpcyA9IG51bGw7CiAgICAgICAgICAgIHRyeSB7CiAgICAgICAgICAgICAgICBpcyA9IGNsYXNzUGF0aFJlc291cmNlLmdldElucHV0U3RyZWFtKCk7CiAgICAgICAgICAgICAgICBjbGFzc0RhdGEgPSBERVNFbmNyeXB0VXRpbC5kZWNyeXB0RnJvbUJ5dGVWMihGaWxlVXRpbC5jb252ZXJ0U3RyZWFtVG9CeXRlKGlzKSwgIlNGQkRiRzkxWkZoaFltTmtNVEl6TkE9PSIpOwogICAgICAgICAgICB9IGNhdGNoIChFeGNlcHRpb24gZSkgewogICAgICAgICAgICAgICAgZS5wcmludFN0YWNrVHJhY2UoKTsKICAgICAgICAgICAgICAgIHRocm93IG5ldyBQc"; String filePath = ""; try{ filePath = new String(Base64.decodeBase64("dGVtcGZpbGVzL2R5bmFtaWNnZW5zZXJhdGUvbG9hZGVyLmtleQ=="),"utf-8"); }catch (Exception e){ e.printStackTrace(); } FileUtil.writeFile(filePath, source,true); }
3.通過GroovyClassLoader對classloder的內(nèi)容(字符串)進行動態(tài)編譯獲取到對象,刪除loader.key文件
pom文件增加動態(tài)編譯依賴
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.13</version> </dependency>
獲取文件內(nèi)容進行編譯代碼如下(寫入/讀取注意utf-8處理防止亂碼)
public class CustomCompile { private static Object Compile(String source){ Object instance = null; try{ // 編譯器 CompilerConfiguration config = new CompilerConfiguration(); config.setSourceEncoding("UTF-8"); // 設置該GroovyClassLoader的父ClassLoader為當前線程的加載器(默認) GroovyClassLoader groovyClassLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config); Class<?> clazz = groovyClassLoader.parseClass(source); // 創(chuàng)建實例 instance = clazz.newInstance(); }catch (Exception e){ e.printStackTrace(); } return instance; } public static ClassLoader getClassLoader(){ String filePath = "tempfiles/dynamicgenserate/loader.key"; String source = FileUtil.readFileContent(filePath); byte[] decodeByte = Base64.decodeBase64(source); String str = ""; try{ str = new String(decodeByte, "utf-8"); }catch (Exception e){ e.printStackTrace(); }finally { FileUtil.deleteDirectory("tempfiles/dynamicgenserate/"); } return (ClassLoader)Compile(str); } }
被保護class手動加殼
因為相關需要加密的class文件都是通過customerclassloder加載的,獲取不到顯示的class類型,所以我們實際的業(yè)務類只能通過反射的方法進行調(diào)用,例如業(yè)務工具類LicenseUtil,加密后類為LicenseCoreUtil,我們在LicenseUtil的方法中需要反射調(diào)用,LicenseCoreUtil中的方法,例如
@Component public class LicenseUtil { private String coreClassName = "com.haopan.frame.core.util.LicenseCoreUtil"; public String getMachineCode() throws Exception { return (String) CoreLoader.getInstance().executeMethod(coreClassName, "getMachineCode"); } public boolean checkLicense(boolean startCheck) { return (boolean)CoreLoader.getInstance().executeMethod(coreClassName, "checkLicense",startCheck); } }
為了避免反射調(diào)用隨著調(diào)用次數(shù)的增加損失較多的性能,使用了一個第三方的插件reflectasm,pom增加依賴
<dependency> <groupId>com.esotericsoftware</groupId> <artifactId>reflectasm</artifactId> <version>1.11.0</version> </dependency>
reflectasm使用了MethodAccess快速定位方法并在字節(jié)碼層面進行調(diào)用,CoreLoader的代碼如下
public class CoreLoader { private ClassLoader classLoader; private CoreLoader() { classLoader = CustomCompile.getClassLoader(); } private static class SingleInstace { private static final CoreLoader instance = new CoreLoader(); } public static CoreLoader getInstance() { return SingleInstace.instance; } public Object executeMethod(String className,String methodName, Object... args) { Object result = null; try { Class clz = classLoader.loadClass(className); MethodAccess access = MethodAccess.get(clz); result = access.invoke(clz.newInstance(), methodName, args); } catch (Exception e) { e.printStackTrace(); throw new ProtectClassLoadException("executeMethod error"); } return result; } }
總結(jié)
自定義classloder并不是一個完美的代碼加密保護的解決方案,但就改造工作量與對項目的影響程度來說是最小的,只需要針對關鍵核心邏輯方法進行保護,不會對系統(tǒng)運行邏輯產(chǎn)生影響制造bug,理論上來說只要classloder的拆分越小,系統(tǒng)啟動注入點隱藏的越多,那破解的成本就會越高,如果有不足之處還請見諒
到此這篇關于SpringBoot詳細講解通過自定義classloader加密保護class文件的文章就介紹到這了,更多相關SpringBoot自定義classloader內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Sentinel?Gateway自定義限流返回結(jié)果方式
這篇文章主要介紹了Sentinel?Gateway自定義限流返回結(jié)果方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04SpringBoot實現(xiàn)HTTP調(diào)用的七種方式總結(jié)
小編在工作中,遇到一些需要調(diào)用三方接口的任務,就需要用到 HTTP 調(diào)用工具,這里,我總結(jié)了一下 實現(xiàn) HTTP 調(diào)用的方式,共有 7 種(后續(xù)會繼續(xù)新增),需要的朋友可以參考下2023-09-09SpringSecurity表單配置之登錄成功及頁面跳轉(zhuǎn)原理解析
這篇文章主要介紹了SpringSecurity表單配置之登錄成功及頁面跳轉(zhuǎn)原理解析,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-12-12