前言
公司项目要求进行源代码加密,防止他人进行反编译(毕竟项目要运行在客户的机器上)。项目框架采用的是: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()) {
f