SpringMVC项目源代码加密

背景

前段时间有个项目快做完时老板要求上线的时候项目代码必须加密(我们的项目是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,等以后有时间了再仔细研究下吧!!

你可能感兴趣的:(Java)