1. 程式人生 > >SpringMVC專案原始碼加密

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、之前有幾位網友私信我說需要讀取配置檔案的程式碼,其實讀配置檔案的方式很多,網上隨便搜下就有。我先把我的讀取配置檔案和解密的程式碼放上來。

讀取配置檔案的類: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,等以後有時間了再仔細研究下吧!!