该文章所涉及的代码,可以在此处下载:https://github.com/chengqianbygithub/JavaLearningDemos/tree/develop
在develop分支哦。
在日常开发中,可能会遇到要对系统中比较敏感的代码进行保护,下面就总结一下保护源码的方法中最简单的方式,即文件加密
加密和解密的大致思想
加密无非就是对class文件进行异或一下,解密呢,那就是再对class文件异或回来即可。
加密后的文件如果想要用到的话,就需要classLoader动态加载进来
具体实现详情:Java实现自定义classLoader类加载器动态解密class文件
1、加解密的实现方式
下面为加解密的实现方式,大部分的解释都放在代码注释里面了。
/**
* 加解密类
*/
public class EdCipher {
// 装加密后的Class文件的目录名
private String encryptFolderName = "encrypt";
/**
* 加密方法
* @param name 需要加密的文件名
*/
public void encryptClass(String name) {
// 获取加密前文件的绝对路径
String path = getAbsolutePathBeforeEncryption(name);
// 为待加密的class文件创建File对象
File classFile = new File(path);
if (!classFile.exists()) {
// TODO 如果文件不存在,做相应的处理。一般情况下都是抛出异常;
} else {
// 在 待加密的文件也就是classFile的同级目录下创建一个文件夹,然后把加密后的文件放进去
File encryptFolder = new File(classFile.getParent() + File.separator + encryptFolderName);
if (!encryptFolder.exists()) {
encryptFolder.mkdirs();
}
}
// 加密后的文件需要单独放,就放在encryptFolderName文件夹下,encryptedFile 为加密后文件的全路径文件名
String encryptedFile = classFile.getParent() + File.separator + encryptFolderName + File.separator + classFile.getName();
try (
FileInputStream fileInputStream = new FileInputStream(classFile);
BufferedInputStream bis = new BufferedInputStream(fileInputStream);
FileOutputStream fileOutputStream = new FileOutputStream(encryptedFile);
BufferedOutputStream bos = new BufferedOutputStream(fileOutputStream)
) {
// 加密
int data;
while ((data = bis.read()) != -1) {
bos.write(data ^ 0xFF);
}
bos.flush();
} catch (IOException e) {
e.printStackTrace();
}
// 现在将原来未加密的文件删除
classFile.delete();
// 先获取加密前文件绝对路径名,然后修改后缀,再创建File对象
File oldFile = new File(path + "en");
// 删除未加密前的文件
if (oldFile.exists()) {
oldFile.delete();
}
// 根据加密后的文件创建File对象
File newEncryptedFile = new File(encryptedFile);
// 将加密后的对象重命名,这时加密后的文件就把加密前的文件替换掉了,这就是为什么刚开始加密后的文件需要单独放的原因
newEncryptedFile.renameTo(oldFile);
// 删除之前的加密文件夹下面的加密文件
newEncryptedFile.getParentFile().delete();
}
/**
* 解密方法
* @param name 需要解密的文件名
*/
protected byte[] decryptClass(String name) {
String path;
if (!name.contains(".class")) {
path = getAbsolutePathAfterEncryption(name);
} else {
path = name;
}
// 为待加密的文件创建File对象
File decryptClassFile = new File(path);
if (!decryptClassFile.exists()) {
System.out.println("decryptClass() File:" + path + " not found!");
return null;
}
byte[] result = null;
BufferedInputStream bis = null;
ByteArrayOutputStream bos = null;
try {
bis = new BufferedInputStream(new FileInputStream(decryptClassFile));
bos = new ByteArrayOutputStream();
// 解密
int data;
while ((data = bis.read()) != -1) {
bos.write(data ^ 0xFF);
}
bos.flush();
result = bos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 获取加密前文件的绝对路径
* @param name 待加密的文件的全限定名
* @return 文件的绝对路径
* 注意:根据待加密的文件的位置不同(外部class,项目内部class),改写改方法。
* 最终目的就是获取到待加密的文件的绝对路径
*/
private String getAbsolutePathBeforeEncryption(String name) {
/*String className = name.substring(name.lastIndexOf(".") + 1) + ".class";
String path = EdCipher.class.getResource(className).toString();
path = path.substring(path.indexOf("file:/") + "file:/".length());
if (System.getProperty("os.name").toUpperCase().contains("LINUX")) {
path = File.separator + path;
}*/
return new File(name).getAbsolutePath();
}
/**
* 获取加密后文件的绝对路径
* @param name 待解密的文件的全限定名
* @return 文件的绝对路径
* 注意:根据加密后文件的位置不同(外部class,项目内部class),改写改方法。
* 最终目的就是获取到加密后文件的绝对路径
*/
private String getAbsolutePathAfterEncryption(String name) {
/*String className = name.substring(name.lastIndexOf(".") + 1) + ".classen";
String path = EdCipher.class.getResource(className).toString();
path = path.substring(path.indexOf("file:/") + "file:/".length());*/
return new File(name).getAbsolutePath();
}
// 测试
public static void main(String[] args) {
EdCipher edCipher = new EdCipher();
edCipher.encryptClass("F:\\Test.class");
// 如果使用Ant打包就可以将上面这句huanchen换成下面这句话。
// edCipher.encryptClass(args[0]);
}
}
1.1、小技巧。
在加密方法的最后为什么要修改文件的后缀呢?
这里在文件后面加了一个“en”,最后生成的文件就是xxx.classen,这样做的目的是为了在JVM的Application ClassLoader类加载器加载classpath下面的class字节码时,区别于普通的class字节码文件,因为该文件已经被加密了。如果类加载器将它加载进了虚拟机,那么在进行类加载过程中的验证阶段中的文件格式验证环节的时候,就会验证出不符合Class文件格式的约束,虚拟机就会抛出一个java.lang.VerifyError异常或其子类异常,假设使用的HTTP服务器是tomcat,那么tomcat就会报错,启动不起来。这算是一个小技巧。
2、拓展:Ant打包时加密
如果要想在Ant打包的时候,就加密文件,就需要在build.xml配置文件中调用该类的Main方法即可;
<!-- 加密 -->
<target name="encrypt">
<java classname="EdCipher" failonerror="true">
<classpath refid="classpath.run"/>
<arg line="需要加密的文件的包名+文件名"/>
</java>
</target>
技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。