今天小編給大家分享一下SpringBoot怎么通過自定義classloader加密保護class文件的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
最近針對公司框架進行關鍵業務代碼進行加密處理,防止通過jd-gui等反編譯工具能夠輕松還原工程代碼,相關混淆方案配置使用比較復雜且針對springboot項目問題較多,所以針對class文件加密再通過自定義的classloder進行解密加載,此方案并不是絕對安全,只是加大反編譯的困難程度,防君子不防小人,整體加密保護流程圖如下圖所示

使用自定義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文件,為了防止在遞歸查找中重復加密,需要對已經加密后的class名稱記錄防止重復
2.在刪除源文件時可能出現編譯占用的情況,執行System.gc()后方可刪除
3.針對自定義插件的列表形式的configuration節點可以使用List來映射
插件使用配置如圖所示

創建CustomClassLoader繼承自ClassLoader,重寫findClass方法只處理裝載加密后的class文件,其他class交有默認加載器處理,需要注意的是默認處理不能調用super.finclass方法,在idea調試沒問題,打成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);
//先查詢有沒有加載過這個類。如果已經加載,則直接返回加載好的類。如果沒有,則加載新的類。
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加密class文件處理方案的漏洞在于自定義類加載器是完全暴露的,只需進行分析解密流程就能獲取到原始class文件,所以我們需要對classloder的內容進行隱藏
1.把classloader的源文件在編譯期間進行刪除(maven自定義插件實現)
2.將classloder的內容進行base64編碼后拆分內容尋找多個系統啟動注入點寫入到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的內容(字符串)進行動態編譯獲取到對象,刪除loader.key文件
pom文件增加動態編譯依賴
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.13</version> </dependency>
獲取文件內容進行編譯代碼如下(寫入/讀取注意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);
// 創建實例
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文件都是通過customerclassloder加載的,獲取不到顯示的class類型,所以我們實際的業務類只能通過反射的方法進行調用,例如業務工具類LicenseUtil,加密后類為LicenseCoreUtil,我們在LicenseUtil的方法中需要反射調用,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);
}
}為了避免反射調用隨著調用次數的增加損失較多的性能,使用了一個第三方的插件reflectasm,pom增加依賴
<dependency> <groupId>com.esotericsoftware</groupId> <artifactId>reflectasm</artifactId> <version>1.11.0</version> </dependency>
reflectasm使用了MethodAccess快速定位方法并在字節碼層面進行調用,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;
}
}以上就是“SpringBoot怎么通過自定義classloader加密保護class文件”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。