Spring项目源代码加密

前言

公司项目要求进行源代码加密,防止他人进行反编译(毕竟项目要运行在客户的机器上)。项目框架采用的是:Spring + Spring MVC + Spring Data JPA。可在网上查阅资料,关于Spring项目源代码加密的内容不多,也没找到什么现成的工具。所以,只能自己动手写加密代码了。过程几经坎坷,在此进行记录一下,也希望能帮到有相同需求的朋友。

思路

写工具类手动对项目指定包下生成的class文件内容进行加密,在容器框架加载class文件时解密获取正确的字节码。头疼的来了,不少地方都对字节码文件进行了读取解析,要改不少源代码。

准备工作

1、准备JDK1.8源码(我电脑配置的JDK为1.8.0_131)。在JDK安装目录下有个src.zip,那个就是当前版本JDK的源码

2、准备Tomcat7源码。(需要安装ant进行项目构建)

3、准备Spring源码。这个下载的框架里面自带的

4、准备hibernate-entitymanager源码

5、准备aspectjweaver源码(项目中切面编程做日志) 

5、一个加密解密的工具类jar包。用于对生成的class文件进行加密,在我们修改后的tomcat、spring中需要调用这个类对已加密的class进行解密。另外这个jar包最后需要使用加密锁进行壳加密以保证加解密代码的安全

6、写一个读取配置文件的工具类。配置文件中记录需要解密的包名、路径地址、是否执行解密操作(便于开发时调试)等信息
 

二、需要修改的类 

 

1、JDK中需要修改的类

a)    java.io.FileInputStream:修改后覆盖到rt.jar中对应包里的class文件

2、Tomcat中需要修改的类

a)    org.apache.tomcat.util.bcel.classfile.ClassParser:修改后覆盖到tomcat-coyote.jar中对应包里的class文件

b)    org.apache.catalina.loader.WebappClassLoader:修改后覆盖到catalina.jar中对应包里的class文件

3、Spring中需要修改的类

a)    org.springframework.core.type.classreading.SimpleMetadataReader:修改后覆盖到spring-core-4.3.7.RELEASE.jar中对应包里的class文件

4、hibernate-entitymanager中需要修改的类

a)   org.hibernate.ejb.packaging.AbstractJarVisitor:修改后覆盖hibernate-entitymanager-4.1.8.Final.jar中对应包里的class文件

b)   org.hibernate.ejb.packaging.ExplodedJarVisitor:修改后覆盖hibernate-entitymanager-4.1.8.Final.jar中对应包里的class文件

5、aspectjweaver中需要修改的类

a)   org.aspectj.apache.bcel.classfile.ClassParser:修改后覆盖aspectjweaver.jar中对应包里的class文件

b)   org.aspectj.apache.bcel.util.NonCachingClassLoaderRepository:修改后覆盖aspectjweaver.jar中对应包里的class文件

三、进行修改 

 1、修改JDK提供的java.io.FileInputStream类

目的:提供方法获取class文件的路径,这样才能做后面的解码。

    /**
     * The path of the referenced file
     * (null if the stream is created with a file descriptor)
     */
    private final String path;

    /**
     * 
     * @title: getPath
     * @description: 提供方法让外部能够获取到当前文件路径
     * @author: 陈家宝
     * @version: V1.00
     * @date: 2019年3月11日 下午1:07:44
     * @return
     */
    public String getPath() {
		return path;
	}

 

 2、修改Tomcat的org.apache.tomcat.util.bcel.classfile.ClassParser类

对ClassParser方法进行重写。目的:对配置文件中要求进行解密的class文件经过解密后,再将流给Tomcat。

原方法

public ClassParser(final InputStream inputStream) {
    this.dataInputStream = new DataInputStream(new BufferedInputStream(inputStream, BUFSIZE));
}

修改后

    public ClassParser(final InputStream inputStream) {
    	// TODO 判断该类是否需要解密
    	InputStream newInputStream = inputStream;
    	
    	try {
			//DecodeConf是记录配置信息的类
			//DecodeConf.isRunDecode:记录是否需要执行解密操作
			if (DecodeConf.getConf().isRunDecode() && inputStream instanceof FileInputStream) {
				// 配置中设置为需要解密,从文件流获取文件路径,用于判断是否为需要解密的类
				// FileInputStream.getPath()为修改JDK源码而来,所有要用我的JDK
				String path = ((FileInputStream)inputStream).getPath();
				FileOperateHelper.log("ClassParser path=" + path, true);
				/*
	               *DecodeConf.dirs:所有需要解密的文件路径(配置文件中记录到目录这层,根据需要可以明确到文件)集合
	               *dirs是一个集合对象,记录了所有需要解密的目录
	               *我配置文件中记录的是相对路径,只到包名这层
	               *如包名为com.abc.service则记录的目录路径为/com/abc/service/
	               *判断当前目录是否需要解密
	              	*/
				for(String dir : DecodeConf.getConf().getDirs()) {
					// 统一文件路径分隔符
					dir = dir.replace("/", "\\");
					FileOperateHelper.log("ClassParser dir=" + dir, true);
					if (path.indexOf(dir) != -1) {
						// 该类在需要解密的目录下,进行解密
						// 读取文件内容
						byte[] oldcontent = FileOperateHelper.read(new File(path));
						// 进行解密
						byte[] decodeByte = EncodeUtil.simpleDecrypt(oldcontent);
						
						newInputStream = new ByteArrayInputStream(decodeByte);
						break;
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
    	
    	// 不管上面有没有解密,用newInputStream生成DataInputStream
        this.dataInputStream = new DataInputStream(new BufferedInputStream(newInputStream, BUFSIZE));
    }

3、修改Tomcat的org.apache.catalina.loader.WebappClassLoader类

在类中重写下findClass方法

    @Override
    public Class findClass(String name) throws ClassNotFoundException {

    	/**
    	 * 类加载器的三个机制:委托、单一性、可见性
    	 * 委托:指加载一个类的请求交给父类加载器,若父类加载器不可以找到或者加载到,再加载这个类
    	 * 单一性:指子类加载器不会再次加载父类加载器已经加载过的类
    	 * 可见性:子类加载器可以看见父类加载器加载的所有类,而父类加载器不可以看见子类加载器所加载的类
    	 * 
    	 * 所以,return super.findClass(name);是可能加载不到类的(可能有些类需要子类加载器才加载到),
    	 * 即意味着可能会产生ClassNotfoundException异常,所以不能放在try catch代码块里边,因为调用者需要知道是否成功加载。
    	 */
    	// 如果配置文件中配置了不执行解密,直接调用父类的findClass
    	if (!DecodeConf.getConf().isRunDecode()) {
    		return super.findClass(name);
    	}
    	
    	try {
			// TODO 判断当前类是否需要解密
			// 判断当前类是否在需要解密的包路径之下
			for(String pkg : DecodeConf.getConf().getPackages()) {
				if (name.indexOf(pkg) != -1) {
					// 将类名称转换为文件路径及文件名
					String fileName = name.replace(".", "/") + ".class";
					// 根据文件路径,从已加载的类的ResourceEntry集合中获取指定的ResourceEntry
					ResourceEntry resourceEntry = resourceEntries.get("/" + fileName);
					// 拼接class文件绝对路径,当然,ResourceEntry中也有文件的绝对路径
					String basePath = DecodeConf.getConf().getClassPath();
					String classPath = basePath + fileName;
					// 但是,内部类获取不到ResourceEntry(不确定是不是全部的内部类都获取不到),如果获取得到就用ResourceEntry中的路径
					if (resourceEntry != null) {
						// 如果路径中带空格会变成"%20"导致无法成功获取文件,进行处理
						classPath = URLDecoder.decode(resourceEntry.source.getPath(), "UTF-8");
						if (classPath.startsWith("/")) {
							classPath = classPath.substring(1);
						}
					}
					
					File classFile = new File(classPath);
					// 如果文件存在,则进行解密
					if (classFile.exists() && classFile.isFile()) {
						InputStream is = StreamDecode.decode(new FileInputStream(classFile));
						// 将解密后的流包含的内容,读取到字节数组
						byte[] b = new byte[is.available()];
						is.read(b);
						// 将字节数组内容转换成Class对象
						return defineClass(b, 0, b.length);
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
    	
    	return super.findClass(name);
    }

4、修改Spring的org.springframework.core.type.classreading.SimpleMetadataReader类

原方法

SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException {
    InputStream is = new BufferedInputStream(resource.getInputStream());
    ClassReader classReader;
    try {
        classReader = new ClassReader(is);
    }
    catch (IllegalArgumentException ex) {
        throw new NestedIOException("ASM ClassReader failed to parse class file - " +
        "probably due to a new Java class file version that isn't supported yet: " + resource, ex);
    }
    finally {
        is.close();
    }
    AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
    classReader.accept(visitor, ClassReader.SKIP_DEBUG);
    this.annotationMetadata = visitor;
    // (since AnnotationMetadataReadingVisitor extends ClassMetadataReadingVisitor)
    this.classMetadata = visitor;
    this.resource = resource;
}

修改后

SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException {
		InputStream is = new BufferedInputStream(resource.getInputStream());
		InputStream newInputStream = is;
		ClassReader classReader;
		try {
			try {
				//TODO 判断是否需要解密
				FileOperateHelper.log("isRunDecode() = " + String.valueOf(DecodeConf.getConf().isRunDecode()), true);
				if(DecodeConf.getConf().isRunDecode()) {
					for(int i=0; i

5、修改hibernate-entitymanager中的org.hibernate.ejb.packaging.AbstractJarVisitor类

原方法

private boolean checkAnnotationMatching(InputStream is, JavaElementFilter filter) throws IOException {
		if ( filter.getAnnotations().length == 0 ) {
			is.close();
			return true;
		}

		DataInputStream dstream = new DataInputStream( is);
		ClassFile cf = null;

		try {
			cf = new ClassFile( dstream );
		}
		finally {
			dstream.close();
			is.close();
		}
		boolean match = false;
		AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute( AnnotationsAttribute.visibleTag );
		if ( visible != null ) {
			for ( Class annotation : filter.getAnnotations() ) {
				match = visible.getAnnotation( annotation.getName() ) != null;
				if ( match ) break;
			}
		}
		return match;
	}

修改后

private boolean checkAnnotationMatching(InputStream is, JavaElementFilter filter) throws IOException {
		if ( filter.getAnnotations().length == 0 ) {
			is.close();
			return true;
		}

		InputStream newInputStream = is;
		// TODO 检查该类是否需要进行解密
		try {
			//DecodeConf是记录配置信息的类
			//DecodeConf.isRunDecode:记录是否需要执行解密操作
			FileOperateHelper.log("AbstractJarVisitor isRunDecode()=" + String.valueOf(DecodeConf.getConf().isRunDecode()), true);
			FileOperateHelper.log("AbstractJarVisitor inputStream tpye: " + is.getClass().getSimpleName(), true);
			if (DecodeConf.getConf().isRunDecode() && is instanceof MyFileInputStream) {
				// 配置中设置为需要解密,从文件流获取文件路径,用于判断是否为需要解密的类
				// FileInputStream.getPath()为修改JDK源码而来,所有要用我的JDK
				String path = "";
				MyFileInputStream fis = (MyFileInputStream)is;
				path = fis.getPath();
				FileOperateHelper.log("AbstractJarVisitor path=" + path, true);
				/*
			       *DecodeConf.dirs:所有需要解密的文件路径(配置文件中记录到目录这层,根据需要可以明确到文件)集合
			       *dirs是一个集合对象,记录了所有需要解密的目录
			       *我配置文件中记录的是相对路径,只到包名这层
			       *如包名为com.abc.service则记录的目录路径为\\com\\abc\\service\\
			       *判断当前目录是否需要解密
		      	 */
				for(String dir : DecodeConf.getConf().getDirs()) {
					// 统一文件路径分隔符
					dir = dir.replace("/", "\\");
					FileOperateHelper.log("AbstractJarVisitor dir=" + dir, true);
					if (path.indexOf(dir) != -1) {
						// 该类在需要解密的目录下,进行解密
						// 读取文件内容
						byte[] oldcontent = FileOperateHelper.read(new File(path));
						// 进行解密
						byte[] decodeByte = EncodeUtil.simpleDecrypt(oldcontent);
						
						newInputStream = new ByteArrayInputStream(decodeByte);
						break;
					}
				}
			}
		} catch (Exception e) {
			FileOperateHelper.log("AbstractJarVisitor transform inputStream error: " + e.getMessage(), true);
			e.printStackTrace();
		}


		DataInputStream dstream = new DataInputStream( newInputStream );
		ClassFile cf = null;

		try {
			cf = new ClassFile( dstream );
		}
		finally {
			dstream.close();
			is.close();
		}
		boolean match = false;
		AnnotationsAttribute visible = (AnnotationsAttribute) cf.getAttribute( AnnotationsAttribute.visibleTag );
		if ( visible != null ) {
			for ( Class annotation : filter.getAnnotations() ) {
				match = visible.getAnnotation( annotation.getName() ) != null;
				if ( match ) break;
			}
		}
		return match;
	}

6、修改hibernate-entitymanager中的org.hibernate.ejb.packaging.ExplodedJarVisitor类

原方法

private void getClassNamesInTree(File jarFile, String header)
    throws IOException
  {
    File[] files = jarFile.listFiles();
    header = header + "/";
    File[] arr$ = files; int len$ = arr$.length; for (int i$ = 0; i$ < len$; ++i$) { File localFile = arr$[i$];
      if (!(localFile.isDirectory())) {
        String entryName = localFile.getName();
        addElement(header + entryName, new BufferedInputStream(new FileInputStream(localFile)), new BufferedInputStream(new FileInputStream(localFile)));
      }
      else
      {
        getClassNamesInTree(localFile, header + localFile.getName());
      }
    }
  }

修改后

private void getClassNamesInTree(File jarFile, String header) throws IOException {
		File[] files = jarFile.listFiles();
		header = header == null ? "" : header + "/";
		for ( File localFile : files ) {
			if ( !localFile.isDirectory() ) {
				String entryName = localFile.getName();
                // MyFileInputStream继承自java.io.FileInputStream,这样才能通过getPath()拿到文件路径
				addElement(
						header + entryName,
						new MyFileInputStream( localFile ),
						new MyFileInputStream( localFile )
				);

			}
			else {
				getClassNamesInTree( localFile, header + localFile.getName() );
			}
		}
	}

7、修改aspectjweaver中的org.aspectj.apache.bcel.classfile.ClassParser类

原方法

  /** Parse class from the given stream */
  public ClassParser(InputStream file, String filename) {
    this.filename = filename;

    if (file instanceof DataInputStream) this.file = (DataInputStream)file;
    else                                 this.file = new DataInputStream(new BufferedInputStream(file,BUFSIZE));
  }

  public ClassParser(ByteArrayInputStream baos, String filename) {
	    this.filename = filename;
	    this.file = new DataInputStream(baos);
  }

  /** Parse class from given .class file */
  public ClassParser(String file_name) throws IOException {    
    this.filename = file_name;
    file = new DataInputStream(new BufferedInputStream(new FileInputStream(file_name),BUFSIZE));
  }

修改后

  /** Parse class from the given stream */
  public ClassParser(InputStream file, String filename) {
    this.filename = filename;

    // 判断是否需要对流进行解密
    file = decryptStream(file);

    if (file instanceof DataInputStream) this.file = (DataInputStream)file;
    else                                 this.file = new DataInputStream(new BufferedInputStream(file,BUFSIZE));
  }

  public ClassParser(ByteArrayInputStream baos, String filename) {
	    this.filename = filename;
	    this.file = new DataInputStream(baos);
  }

  /** Parse class from given .class file */
  public ClassParser(String file_name) throws IOException {    
    this.filename = file_name;
    MyFileInputStream mis = new MyFileInputStream(new File(file_name));
    // 判断是否需要对流进行解密
    InputStream is = decryptStream(mis);
    file = new DataInputStream(new BufferedInputStream(is,BUFSIZE));
  }

  // 判断是否需要解密,对要解密的流进行解密操作
  public InputStream decryptStream(InputStream inputStream) {
    InputStream newInputStream = inputStream;
      
      try {
      //DecodeConf是记录配置信息的类
      //DecodeConf.isRunDecode:记录是否需要执行解密操作
      if (DecodeConf.getConf().isRunDecode() && inputStream instanceof MyFileInputStream) {
        // 配置中设置为需要解密,从文件流获取文件路径,用于判断是否为需要解密的类
        // FileInputStream.getPath()为修改JDK源码而来,所有要用我的JDK
        String path = ((MyFileInputStream)inputStream).getPath();
        FileOperateHelper.log("aspectjweaver  ClassParser path=" + path, true);
        /*
         *DecodeConf.dirs:所有需要解密的文件路径(配置文件中记录到目录这层,根据需要可以明确到文件)集合
         *dirs是一个集合对象,记录了所有需要解密的目录
         *我配置文件中记录的是相对路径,只到包名这层
         *如包名为com.abc.service则记录的目录路径为\\com\\abc\\service\\
         *判断当前目录是否需要解密
           */
        for(String dir : DecodeConf.getConf().getDirs()) {
          // 统一文件路径分隔符
          dir = dir.replace("/", "\\");
          FileOperateHelper.log("aspectjweaver   ClassParser dir=" + dir, true);
          if (path.indexOf(dir) != -1) {
            // 该类在需要解密的目录下,进行解密
            // 读取文件内容
            byte[] oldcontent = FileOperateHelper.read(new File(path));
            // 进行解密
            byte[] decodeByte = EncodeUtil.simpleDecrypt(oldcontent);
            
            newInputStream = new ByteArrayInputStream(decodeByte);
            return newInputStream;
          }
        }
      }
    } catch (Exception e) {
      e.printStackTrace();
    }

    return null;
  }

8、修改aspectjweaver中的org.aspectj.apache.bcel.util.NonCachingClassLoaderRepository类

原方法

	private JavaClass loadJavaClass(String className) throws ClassNotFoundException {
		String classFile = className.replace('.', '/');
		try {
			 InputStream is = loaderRef.getClassLoader().getResourceAsStream(classFile + ".class");
			
			if (is == null) {
				throw new ClassNotFoundException(className + " not found.");
			}

			ClassParser parser = new ClassParser(is, className);
			return parser.parse();
		} catch (IOException e) {
			throw new ClassNotFoundException(e.toString());
		}
	}

修改后

	private JavaClass loadJavaClass(String className) throws ClassNotFoundException {
		String classFile = className.replace('.', '/');
		try {
			// InputStream is = loaderRef.getClassLoader().getResourceAsStream(classFile + ".class");
			//TODO 命令行编译时,FileInputStream.getPath()方法获取不到,所以这里替换成我的MyFileInputStream
			URL url = loaderRef.getClassLoader().getResource(classFile + ".class");
			// 空格会显示为"%20",导致获取不到文件
			String path = url.getPath();
			path = URLDecoder.decode(path, "UTF-8");
			MyFileInputStream is = new MyFileInputStream(new File(path));

			if (is == null) {
				throw new ClassNotFoundException(className + " not found.");
			}

			ClassParser parser = new ClassParser(is, className);
			return parser.parse();
		} catch (IOException e) {
			throw new ClassNotFoundException(e.toString());
		}
	}

 

四、关于修改的源码编译问题

我的话,Tomcat7用myeclipse搭建了源码项目,所以编译不成问题。其他的源码,修改过后用命令行对修改的java文件进行编译。示例如下:

C:\Users\Administrator\Desktop\codeEncrypt\aspectjweaver\aspectjweaver-1.7.2-sou
rces\org\aspectj\apache\bcel\util>javac -encoding utf-8 -Djava.ext.dirs=C:\Users
\Administrator\Desktop\codeEncrypt\aspectjweaver\needJar NonCachingClassLoaderRe
pository.java

 解释:-encoding   指定源文件使用的字符编码。因为文件中使用到中文注释,这里指定utf-8;

           -Djava.ext.dirs   指定编译时要引用到的jar包路径,建议把所有引用到的jar包放在同一个文件夹

另外,命令行编译java文件时,不知道为什么,获取不到JDK中为java.io.FileInputStream添加的getPath()方法,编译无法通过。我确定已经替换了当前配置的JDK的FileInputStream.class文件。而且,Tomcat7的源码修改中,也用到了该方法。但是用myeclipse编译,并没有问题。最后,采取的解决方案是:新建一个类MyFileInputStream,继承自java.io.FileInputSteam。这样,在编译时就没有问题了。无法理解,希望知道缘由的朋友给予指点。

 

五、配置文件

通过配置文件记录加/解密的信息,放置在tomcat的lib目录下。例如:

Spring项目源代码加密_第1张图片

文件内容:

#是否运行解密
isRunDecode=true
#加密文件相对目录(就是包名转成的目录),多个用“,”分割
#dirs=\\com\\smt\\iptv\\portal\\web\\json
dirs=/com/smt/iptv/portal/web/json
#加密文件的包名,多个用“,”分割
packages=com.smt.iptv.portal.web.json
#加密文件的绝对路径
classPath=C:\\Develop\\apache-tomcat-7.0.93\\webapps\\iptvmanager_maven_test\\WEB-INF\\classes

 

下面是的工具类、加/解密类等,建议新建java项目,放在一起,打成jar包,编译修改的源码时要用到。另外,也要放一份在Tomcat的lib目录,项目运行需要。

 读取配置文件的工具类:

package com.jack.vesoft.classencrypt.classreading;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;

/**
 * 
 * Title: DecodeConf
 * Description: 读取配置文件
 * @author 陈家宝
 * @date   2019年3月11日 下午1:50:31
 *
 */
public class DecodeConf {
	
	private Properties props = new Properties();
	
	private boolean isRunDecode = false;
	
	private String[] dirs;		// 需要解密的路径(包的文件夹层级)
	
	private String[] packages;	// 需要解密的包
	
	private String classPath;	// 加密文件的绝对路径
	
	private static DecodeConf decodeConf;
	
	public Properties getProps(){
		return props;
	}
	
	public boolean isRunDecode() {
		return isRunDecode;
	}
 
	public void setRunDecode(boolean isRunDecode) {
		this.isRunDecode = isRunDecode;
	}
 
	public String[] getDirs() {
		return dirs;
	}
 
	public void setDirs(String[] dirs) {
		this.dirs = dirs;
	}
 
	public String[] getPackages() {
		return packages;
	}
 
	public void setPackages(String[] packages) {
		this.packages = packages;
	}
 
	public String getClassPath() {
		return classPath;
	}
 
	public void setClassPath(String classPath) {
		this.classPath = classPath;
	}
	
	public static DecodeConf getConf(){
		return decodeConf;
	}
 
    // 默认读取类路径下的decode.properties文件,所以此工具类所在的jar包,运行时要放在Tomcat的lib目录,decode.properties文件也要放在这里
	static{
		decodeConf = new DecodeConf();
		InputStream is = DecodeConf.class.getClassLoader().getResourceAsStream("decode.properties");
		if (is != null) {
			BufferedReader br = new BufferedReader(new InputStreamReader(is));
			try{
				decodeConf.getProps().load(br);
				decodeConf.setRunDecode(Boolean.parseBoolean(decodeConf.getProps().getProperty("isRunDecode")));
				decodeConf.setDirs(decodeConf.getProps().getProperty("dirs").split(","));
				decodeConf.setPackages(decodeConf.getProps().getProperty("packages").split(","));
				decodeConf.setClassPath(decodeConf.getProps().getProperty("classPath"));
			}catch(Exception e){
				e.printStackTrace();
			}finally {
				try {
					br.close();
					is.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	/**
	 * 
	 * @title: explicitPropertyFilePos
	 * @description: 如果decode.properties不在类路径,调用此方法明确其位置
	 * @author: 陈家宝
	 * @version: V1.00
	 * @date: 2019年3月12日 上午11:33:15
	 * @param filepath
	 */
	public static void explicitPropertyFilePos(String filepath) {
		try {
			InputStream is = new FileInputStream(new File(filepath));
			BufferedReader reader = new BufferedReader(new InputStreamReader(is));
			decodeConf = new DecodeConf();
			decodeConf.getProps().load(reader);
			decodeConf.setRunDecode(Boolean.parseBoolean(decodeConf.getProps().getProperty("isRunDecode")));
			decodeConf.setDirs(decodeConf.getProps().getProperty("dirs").split(","));
			decodeConf.setPackages(decodeConf.getProps().getProperty("packages").split(","));
			decodeConf.setClassPath(decodeConf.getProps().getProperty("classPath"));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	@Override
	public String toString() {
		return "classpath = " + decodeConf.classPath;
	}
}

 

加/解密工具类:

package com.jack.vesoft.classencrypt.main;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;

import com.jack.vesoft.encode.util.FileOperateHelper;
import com.jack.vesoft.encode.util.Utils;

import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * 
 * Title: EncodeUtil
 * Description: 加密工具类
 * @author 陈家宝
 * @date   2019年3月11日 下午1:46:15
 *
 */
public class EncodeUtil {
	
	public static void main(String[] args) throws Exception {  
        /*String content = "admin";  
        System.out.println("加密前:" + content);  
  
        String key = "key";  
        System.out.println("加密密钥和解密密钥:" + key);  
          
        String encrypt = aesEncrypt(content, key);  
        System.out.println("加密后:" + encrypt);  
          
        String decrypt = aesDecrypt(encrypt, key);  
        System.out.println("解密后:" + decrypt); */ 
		
		String content = "w4rDvsK6wr4AAAAzAGIHAAIBADFjb20vc210L2lwdHYvcG9ydGFsL3dlYi9qc29uL0RhdGFDZW50L0RhdGFDZW50ZXJDb250cm9sbGVyBwAEAQAQamF2YS9sYW5nL09iamVjdAEAEmlwdHZwcm9maWxlU2VydmljZQEANExjb20vc210L2lwdHYvcG9ydGFsL3NlcnZpY2UvY21wL0lwdHZwcm9maWxlU2VydmljZTsBABlSdW50aW1lVmlzaWJsZUFubm90YXRpb25zAQA4TG9yZy9zcHJpbmdmcmFtZXdvcmsvYmVhbnMvZmFjdG9yeS9hbm5vdGF0aW9uL0F1dG93aXJlZDsBAAY8aW5pdD4BAAMoKVYBAARDb2RlCgADAA0MAAkACgEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBADNMY29tL3NtdC9pcHR2L3BvcnRhbC93ZWIvanNvbi9EYXRhQ2VudGVyQ29udHJvbGxlcjsBAA5nZXRJcHR2cHJvZmlsZQEAJChMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9MaXN0OwEACVNpZ25hdHVyZQEAOChMamF2YS9sYW5nL1N0cmluZzspTGphdmEvdXRpbC9MaXN0PExqYXZhL2xhbmcvU3RyaW5nOz47AQA4TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL2JpbmQvYW5ub3RhdGlvbi9SZXF1ZXN0TWFwcGluZzsBAAV2YWx1ZQEADC9pcHR2cHJvZmlsZQEABm1ldGhvZAEAN0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9iaW5kL2Fubm90YXRpb24vUmVxdWVzdE1ldGhvZDsBAANHRVQBAAhwcm9kdWNlcwEAHmFwcGxpY2F0aW9uL2pzb247Y2hhcnNldD1VVEYtOAEANkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9i";
		System.out.println(FileOperateHelper.bytesToHexString(base64Decode(content)));
    }  
      
    /** 
     * 将byte[]转为各种进制的字符串 
     * @param bytes byte[] 
     * @param radix 可以转换进制的范围,从Character.MIN_RADIX到Character.MAX_RADIX,超出范围后变为10进制 
     * @return 转换后的字符
     */  
    public static String binary(byte[] bytes, int radix){  
        return new BigInteger(1, bytes).toString(radix);// 
    }  
      
    /** 
     * base 64 encode 
     * @param bytes 待编码的byte[] 
     * @return 编码后的base 64 code 
     */  
    public static String base64Encode(byte[] bytes){  
        return new BASE64Encoder().encode(bytes);  
    }  
      
    /** 
     * base 64 decode 
     * @param base64Code 待解码的base 64 code 
     * @return 解码后的byte[] 
     * @throws Exception 
     */  
    public static byte[] base64Decode(String base64Code) throws Exception{  
        return Utils.isEmptyString(base64Code) ? null : new BASE64Decoder().decodeBuffer(base64Code);  
    }  
      
    /** 
     * 获取byte[]的md5
     * @param bytes byte[] 
     * @return md5 
     * @throws Exception 
     */  
    public static byte[] md5(byte[] bytes) throws Exception {  
        MessageDigest md = MessageDigest.getInstance("MD5");  
        md.update(bytes);  
          
        return md.digest();  
    }  
      
    /** 
     * 获取字符串md5
     * @param msg  
     * @return md5 
     * @throws Exception 
     */  
    public static byte[] md5(String msg) throws Exception {  
        return Utils.isEmptyString(msg) ? null : md5(msg.getBytes());  
    }  
      
    /** 
     * 结合base64实现md5加密 
     * @param msg 待加密字符串 
     * @return 获取md5后转为base64 
     * @throws Exception 
     */  
    public static String md5Encrypt(String msg) throws Exception{  
        return Utils.isEmptyString(msg) ? null : base64Encode(md5(msg));  
    }  
      
    /** 
     * AES加密 
     * @param content 待加密的内容 
     * @param encryptKey 加密密钥 
     * @return 加密后的byte[] 
     * @throws Exception 
     */  
    public static byte[] aesEncryptToBytes(String content, String encryptKey) throws Exception {  
        KeyGenerator kgen = KeyGenerator.getInstance("AES");  
        kgen.init(128, new SecureRandom(encryptKey.getBytes()));  
  
        Cipher cipher = Cipher.getInstance("AES");  
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(kgen.generateKey().getEncoded(), "AES"));  
          
        return cipher.doFinal(content.getBytes("utf-8"));  
    }  
      
    /** 
     * AES加密为base 64 code 
     * @param content 待加密的内容 
     * @param encryptKey 加密密钥 
     * @return 加密后的base 64 code 
     * @throws Exception 
     */  
    public static String aesEncrypt(String content, String encryptKey) throws Exception {  
        return base64Encode(aesEncryptToBytes(content, encryptKey));  
    }  
      
    /** 
     * AES解密 
     * @param encryptBytes 待解密的byte[] 
     * @param decryptKey 解密密钥 
     * @return 解密后的String 
     * @throws Exception 
     */  
    public static String aesDecryptByBytes(byte[] encryptBytes, String decryptKey) throws Exception {  
        KeyGenerator kgen = KeyGenerator.getInstance("AES");  
        kgen.init(128, new SecureRandom(decryptKey.getBytes()));  
          
        Cipher cipher = Cipher.getInstance("AES");  
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(kgen.generateKey().getEncoded(), "AES"));  
        byte[] decryptBytes = cipher.doFinal(encryptBytes);  
          
        return new String(decryptBytes);  
    }  
      
    /** 
     * 将base 64 code AES解密 
     * @param encryptStr 待解密的base 64 code 
     * @param decryptKey 解密密钥 
     * @return 解密后的string 
     * @throws Exception 
     */  
    public static String aesDecrypt(String encryptStr, String decryptKey) throws Exception {  
        return Utils.isEmptyString(encryptStr) ? null : aesDecryptByBytes(base64Decode(encryptStr), decryptKey);  
    }  
    
    /**
     * 
     * @title: myEncrypt
     * @description: 简单加密方式,每一位byte加一
     * @author: 陈家宝
     * @version: V1.00
     * @date: 2019年3月13日 上午1:51:56
     * @param buf
     * @return
     */
    public static byte[] simpleEncrypt(byte[] buf) {
    	if (buf == null || buf.length == 0) {
    		return null;
		}
    	byte[] result = new byte[buf.length];
    	for(int i=0; i

 方法虽然挺多的,但实际用到的就后面两个:simpleDecrypt(byte[] oldBytes)方法和simpleEncrypt(byte[] buf)方法。之前也是想采用什么base64、aes等算法对class文件内容进行加/解密。遗憾的是,没能做出来。因为使用那些算法涉及到了字符编码的问题,在读写class文件、new String()等细节上,虽然指定了编码,但最后发现文件内容还是不正确。主要是class文件的标志(MAGIC)出现了问题,文件后面的内容加解密正常。无奈之下,全程使用byte字节进行操作,不涉及字符编码,加/解密就成功实现了。

 

另外一个解密工具类:

package com.jack.vesoft.classencrypt.main;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;

import com.jack.vesoft.encode.util.FileOperateHelper;
 
/**
 * 
 * Title: StreamDecode
 * Description: 解密工具类
 * @author 陈家宝
 * @date   2019年3月11日 下午1:54:01
 *
 */
public class StreamDecode {
 
	public static InputStream decode(InputStream is) throws Exception{
		byte[] oldBytes = FileOperateHelper.read(is);
		byte[] newBytes = EncodeUtil.simpleDecrypt(oldBytes);
		
		// 解密得到的数据
		String hexStr = FileOperateHelper.bytesToHexString(newBytes);
		FileOperateHelper.log("StreamDecode->decode->hexStr=" + hexStr, true);
		
		return new ByteArrayInputStream(newBytes);
	}
	
	public static void main(String[] args) throws Exception {
		String pathname = "C:\\Develop\\apache-tomcat-7.0.93\\webapps\\iptvmanager_maven_test\\WEB-INF\\classes\\com\\smt\\iptv\\portal\\web\\json\\DataCenterController.class";
		InputStream is = new FileInputStream(new File(pathname));
		decode(is);
	}
}

 

 读写文件工具类:

package com.jack.vesoft.encode.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;

/**
 * 
 * Title: FileOperateHelper
 * Description: 文件读写帮助类
 * @author 陈家宝
 * @date   2019年3月12日 下午1:37:01
 *
 */
public class FileOperateHelper {
	
	private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	private static final boolean HIDE_LOG = false;		// 隐藏调试输出
	
	/**
	 * 
	 * @title: read
	 * @description: 读取文件内容
	 * @author: 陈家宝
	 * @version: V1.00
	 * @date: 2019年3月13日 上午2:16:04
	 * @param file
	 * @return
	 * @throws Exception
	 */
	public static byte[] read(File file) throws Exception {
		BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
		byte[] buf = new byte[1024];
		int len = 0;
		List list = new ArrayList();
		while((len = bis.read(buf)) > 0) {
			for(int i=0; i list = new ArrayList();
		while((len = bis.read(buf)) > 0) {
			for(int i=0; i

 

 MyFileInputStream类,为了解决命令行编译修改的源码文件,getPath()获取不到的问题:

package com.jack.vesoft.encode.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class MyFileInputStream extends FileInputStream {

	public MyFileInputStream(File file) throws FileNotFoundException {
		super(file);
	}

	@Override
	public String getPath() {
		// 命令行获取不到java.io.FileInputStream的getPath()方法,该方法是修改JDK而来。看下继承是否有效
		// 事实证明,这么玩是可以的。我能怎么办呢?我也很绝望啊
		return super.getPath();
	}
}

 

 手动对项目class文件进行加密的工具类:

package test;

import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;

import com.jack.vesoft.classencrypt.classreading.DecodeConf;
import com.jack.vesoft.classencrypt.main.EncodeUtil;
import com.jack.vesoft.encode.util.FileOperateHelper;

public class QuickStart {
	public static void main(String[] args) throws Exception {
		// 命令行输入加/解密配置文件绝对路径
		// C:\\Develop\\apache-tomcat-7.0.93\\lib\\decode.properties
		System.out.println("请输入加/解密配置文件绝对路径:");
		InputStreamReader isr = new InputStreamReader(System.in);
		BufferedReader br = new BufferedReader(isr);
		String filepath = br.readLine();
		// 获取配置文件信息
		DecodeConf.explicitPropertyFilePos(filepath);
		
		// 输出配置信息
		String classpath = DecodeConf.getConf().getClassPath();
		String[] dirs = DecodeConf.getConf().getDirs();
		String[] pkgs = DecodeConf.getConf().getPackages();
		
		// 如果开启加密,循坏dirs路径数组,对要加密的路径下的文件递归进行加密
		if (DecodeConf.getConf().isRunDecode()) {
			encryptFileInTargetDirectory(dirs);
		}
	}
	
	/**
	 * 
	 * @title: encryptFileInTargetDirectory
	 * @description: 递归进行加密
	 * @author: 陈家宝
	 * @version: V1.00
	 * @date: 2019年3月12日 上午10:09:18
	 * @param dirs
	 * @throws Exception
	 */
	public static void encryptFileInTargetDirectory(String[] dirs) throws Exception {
		for(int i=0; i entrypt done!");
	}

	private static void encryptFiles(File[] listFiles) throws Exception {
		for (File file : listFiles) {
			if (file.isDirectory()) {
				encryptFiles(file.listFiles());
			}else {
				// 到这说明是文件,进行加密
				encodeAndWriteBack(file);
			}
		}
		
	}
}

 将工具类所在的项目打成jar包后,用压缩包打开jar包,修改\META-INF\MANIFEST.MF文件,添加Main-Class: test.QuickStart指定运行jar时程序入口,修改后如下:

 注意:Main-Class: test.QuickStart(冒号后面有一个空格,整个文件最后有一行空行)

这样,就可以很方便的对项目的class文件进行加密了。

在命令行运行指令:java -jar codeEncryptUtils.jar 完成加密。

运行效果如下:

Spring项目源代码加密_第2张图片

这里示例对DataCenterController.class文件进行加密,没有加密前可以用jd-gui反编译:

 Spring项目源代码加密_第3张图片

加密后:

 Spring项目源代码加密_第4张图片

 

 六、测试运行

根据上述方法修改完成后把JDK、Tomcat、Spring等需要替换的jar备份一下,替换时选中jar包鼠标右键选择使用rar或360压缩工具打开jar包,然后根据“二、需要修改的类”中说明的对应位置进行替换,rar会提示压缩文件已修改是否需要保存,确定保存后就好了(注意:如果直接在JDK或Tomcat目录中打开替换可能会保存失败,需要将jar包复制到其他目录,比如D盘根目录什么的,再根据上面说的方式替换保存,最后将jar包覆盖回原来的地方),最后将加密好的类替换掉原来的类(注意实体类不能加密,否则在运行过程中会报错),把配置文件中isRunDecode设置为true,启动项目看下结果吧!!!

 

 七、局限性

1、不能加密entity(实体类),因为实体类的字节码读取解析用到了JNI调用非JAVA程序完成,能力有限,这部分还没法修改。

具体代码详见java.lang.ClassLoader类:

2、不能加密@WebService类,理由同上。

 

建议只对service和controller加密。

 

 八、总结

做完下来,需要改动的地方不少,在摸索哪些地方需要修改以及如何修改着实花费了不少时间,甚至走了许多弯路,到目前为止感觉还是有很多不是很理解需要进一步研究的地方。如果你的项目用到了别的框架,可能需要改动别的地方,这里主要是提供一个思路,就不放项目链接了。如果有别的不错的方法防止反编译,则不建议这种方法,挺花时间的。

 

 

 

你可能感兴趣的:(spring)