前段时间有个项目快做完时老板要求上线的时候项目代码必须加密(我们的项目是Java开发的Web项目,用的SpringMVC框架),当时考虑最简单的方法就是壳加密,因为之前在其他项目中用过SafeNet的Hasp加密锁,所以考虑还是用这个锁加壳,但是悲剧的是奋斗了几天加一个通宵,把SafeNet的技术支持叫来现场处理都没搞定,后来SafeNet的工程师说由于Sping都是用反射去处理的,所以SpringMVC的项目暂时无法加壳,考虑到SafeNet已经是国际上非常知名的锁供应商了,所以觉得其他锁支持的可能性也比较低。没办法,只能自己动手尝试下代码加密了!花了几天时间在网上找了下相关资料,发现搜索出来的基本都是说的通讯加密,代码加密的文章很少,好不容易找到一篇和代码加密相关的文章吧,还看得云里雾里的。发现自己好像废话说得有点多,反正经过几天努力最后还是搞定了,由于这块不可能经常用到,怕时间一长就忘了怎么弄了,所以趁现在还记得先记录下来以备将来需要。切入正题,开始讲下怎么对SpringMVC项目的源码进行加密。
1、准备JDK1.8源码。在JDK安装目录下有个src.zip,那个就是当前版本JDK的源码
2、准备Tomcat7源码。Tomcat源码可以通过apache的svn获取,Tomcat7.0的svn地址为http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk
3、准备SpringMVC源码。这个下载的框架里面自带的
4、一个加密解密的工具类jar包。用于对生成的class文件进行加密,在我们修改后的tomcat、spring中需要调用这个类对已加密的class进行解密。另外这个jar包最后需要使用加密锁进行壳加密以保证加解密代码的安全
5、写一个读取配置文件的工具类。配置文件中记录需要解密的包名、路径地址、是否执行解密操作(便于开发时调试)等信息
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文件
1、修改java.io.FileInputStream类
为了能够通过FileInputStream获取文件的路径,需要添加一个方法返回当前文件路径,path原来就有,但是没有public方法供外部调用
/* The path of the referenced file (null if the stream is created with a file descriptor) */
private final String path;
public String getPath(){
return path;
}
2、修改org.apache.tomcat.util.bcel.classfile.ClassParser类
重写ClassParser方法
原方法
public ClassParser(final InputStream inputStream) {
this.dataInputStream = new DataInputStream(new BufferedInputStream(inputStream, BUFSIZE));
}
修改后
public ClassParser(final InputStream inputStream) {
InputStream newInputStream = inputStream;
try {
//DecodeConf是记录配置信息的类
//DecodeConf.isRunDecode:记录是否需要执行解密操作
if(DecodeConf.getConf().isRunDecode() && inputStream instanceof FileInputStream){
//获取文件流中文件的路径,用于判断是否是我们需要解密的类
String path = ((FileInputStream)inputStream).getPath();
/*
*DecodeConf.dirs:所有需要解密的文件路径(配置文件中记录到目录这层,根据需要可以明确到文件)集合
*dirs是一个集合对象,记录了所有需要解密的目录
*我配置文件中记录的是相对路径,只到包名这层
*如包名为com.abc.service则记录的目录路径为\\com\\abc\\service\\
*判断当前目录是否需要解密
*/
for(String dir : DecodeConf.getConf().getDirs()){
if(path.indexOf(dir) != -1){
//StreamDecode为用于解密的类
newInputStream = StreamDecode.decode(inputStream);
break;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
this.dataInputStream = new DataInputStream(new BufferedInputStream(newInputStream, BUFSIZE));
}
3、修改org.apache.catalina.loader.WebappClassLoader类
在类中重写下findClass方法
@Override
public Class> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
try{
//不执行解密的话直接调用父的findClass
if(!DecodeConf.getConf().isRunDecode()){
return super.findClass(name);
}
/*
* 判断当前类所在的包是否需要解密
* DecodeConf.packages:记录了所有需要解密的包
*/
for(String pkg : DecodeConf.getConf().getPackages()){
if(name.indexOf(pkg) != -1){
//将类名转为文件路径及文件名
String fileName = name.replace(".", "/") + ".class";
//根据文件名获取ResourceEntry
ResourceEntry resourceEntry = resourceEntries.get("/" + fileName);
//原来没有下面这句代码,但是在运行时发现有些类取不到resourceEntry,导致无法取到文件路径
//有些内部类取不到resourceEntry,文件名为xxxxxx$1.class这种的,不知道是不是所有这种都取不到,等有时间再试试
//DecodeConf.classPath:记录了要解密的class路径
//其实也可以不用ResourceEntry获取路径,直接用上面的配置路径去取就行
String classPath = DecodeConf.getConf().getClassPath() + fileName;
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()){
InputStream is = StreamDecode.decode(new FileInputStream(classFile));
byte[] byts = new byte[is.available()];
is.read(byts);
return defineClass(byts , 0 ,byts.length) ;
}
}
}
}catch(Exception e){
//在最上面“return super.findClass(name);”的这句代码会抛出很多错,不知道为什么
//照理说不走解密时直接调用super方法应该没问题,但是事实是抛出了N多错
//虽然抛出很多错,但是不影响使用,暂时先注释了,等有空了再仔细研究下
//e.printStackTrace();
}
return super.findClass(name);
}
4、修改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());
ClassReader classReader;
try {
try{
if(DecodeConf.getConf().isRunDecode()){
for(String dir : DecodeConf.getConf().getDirs()){
String filepath = "";
//获取文件路径,判断是否是需要解密的class
if(resource instanceof FileSystemResource){
filepath = ((FileSystemResource)resource).getPath();
}else if(resource instanceof ClassPathResource){
filepath = ((ClassPathResource)resource).getPath();
}
if(filepath.indexOf(dir) != -1){
is = StreamDecode.decode(is);
break;
}
}
}
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
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;
}
我们在tomcat和spring中都用到了配置文件中记录的信息去解密(代码中用到DecodeConf的地方),我们需要把配置文件分别放到Tomcat的lib目录和项目的classes目录里,这2个配置文件的内容基本一样,但也有些许差别。
放置在Tomcat中的配置文件
#是否运行解密
isRunDecode=false
#加密文件相对目录(就是包名转成的目录),多个用“,”分割
dirs=\\com\\dataAnalysis\\service,\\com\\dataAnalysis\\action
#加密文件的包名,多个用“,”分割
packages=com.dataAnalysis.service,com.dataAnalysis.action
#加密文件的绝对路径
classPath=C:\\Program Files\\Apache Software Foundation\\Tomcat 7.0\\webapps\\BDODataAnalysis\\WEB-INF\\classes\\
放置在classes中的配置文件
#是否运行解密
isRunDecode=false
#加密文件相对目录(就是包名转成的目录),多个用“,”分割
dirs=com/dataAnalysis/service,com/dataAnalysis/action
#加密文件的包名,多个用“,”分割
packages=com.dataAnalysis.service,com.dataAnalysis.action
需要注意的是两个配置文件dirs中路径使用的斜杠是相反的
PS:1、关于加解密和读取配置文件的方法就不写了,网上一搜一大堆。
2、之前有几位网友私信我说需要读取配置文件的代码,其实读配置文件的方式很多,网上随便搜下就有。我先把我的读取配置文件和解密的代码放上来。
3、需要加密的类不能包含内部类,内部类的加载貌似不是在上面修改的那几个地方,等有空了再研究下
读取配置文件的类:DecodeConf
package org.springframework.core.type.classreading;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;
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;
}
static{
decodeConf = new DecodeConf();
InputStream is = DecodeConf.class.getClassLoader().getResourceAsStream("decode.properties");
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();
}
}
}
加解密用到的类:EncodeUtil
package com.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 sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
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);
}
/**
* 将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);
}
}
解密用到的类:StreamDecode
package com.vesoft.encode.main;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import com.vesoft.encode.util.EnCodeUtil;
public class StreamDecode {
public static InputStream decode(InputStream is) throws Exception{
byte[] oldByts = new byte[is.available()];
is.read(oldByts);
byte[] newByts = EnCodeUtil.base64Decode(new String(oldByts));
return new ByteArrayInputStream(newByts);
}
}
工具类:Utils
package com.vesoft.encode.util;
public class Utils {
public static boolean isEmptyString(String str){
return str == null || str.length() == 0;
}
}
根据上述方法修改完成后把JDK、Tomcat、SpringMVC中需要替换的jar备份一下,替换时选中jar包鼠标右键选择使用rar或360压缩工具打开jar包,然后根据“二、需要修改的类”中说明的对应位置进行替换,rar会提示压缩文件已修改是否需要保存,确定保存后就好了(注意:如果直接在JDK或Tomcat目录中打开替换可能会保存失败,需要将jar包复制到其他目录,比如D盘根目录什么的,再根据上面说的方式替换保存,最后将jar包覆盖回原来的地方),最后将加密好的类替换掉原来的类(注意实体类不能加密,否则在运行过程中会报错),把2个配置文件中isRunDecode设置为true,启动项目看下结果吧!!!
总体来说需要改动的地方并不多,但是摸索哪些地方需要修改以及如何修改着实花费了不少时间,甚至走了许多弯路,到目前为止感觉还是有很多不是很理解需要进一步研究的地方,比如有些情况下需要使用配置文件中的路径(配置文件中的classPath)才能找到加密的class,考虑是否可以完全脱离配置文件就能定位到所有class,等以后有时间了再仔细研究下吧!!