SHIHUC

好记性不如烂笔头,还可以分享给别人看看! 专注基础算法,互联网架构,人工智能领域的技术实现和应用。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

基于Spring的placeholder处理思路,实现系统配置信息敏感信息的加密解密处理。

 

我们的处理方案,是基于类org.springframework.beans.factory.config.PropertiesFactoryBean进行重写,嵌入密文信息的解密逻辑,灵活处理各种敏感信息的加解密,而且加解密算法,可以根据需要自己灵活设计。

 

1. 首先,设计敏感信息的加解密算法程序,这里,就基于JDK自带的工具,基于AES算法进行加密encrypt和解密dencrypth操作

package com.tk.robotbi.core.engine;

import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;

/**
 * @author shihuc
 * @date  2018年3月8日 上午9:20:55
 * 
 * 通过JDK自带的AES加解密方法,对系统配置参数需要防范的信息进行处理
 * 
 */
public class JdkAesHelper {
    
    private static String ALGORITHM = "AES";
    private static String TRANSFORMATION = "AES/ECB/PKCS5Padding";
    private static int KEY_SIZE = 128;
    
    /*
     * 这里的seed值,有点类似加密中的干扰项,使得密码更加不容易破解,可以当作私钥信息使用,即不知道这个信息,AES解密也无法进行。
     */
    private static String SEED = "xxxxxxxx***************";
    
    public static String doEncrypt(String toEnc, String seed) {
        // 生成密钥  
        KeyGenerator keyGenerator = null;
        try {
            keyGenerator = KeyGenerator.getInstance(ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } 
        System.out.println(keyGenerator.getProvider());  
        /*
         * 指定key的长度为128位
         * 给定种子作为随机数初始化出。若不指定种子信息,则秘钥生产器初始化将会完全随机,最后无法解密
         */
        SecureRandom random = null;
        try {
            random = SecureRandom.getInstance("SHA1PRNG");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        random.setSeed(seed.getBytes());
        keyGenerator.init(KEY_SIZE, random);
        SecretKey secretKey = keyGenerator.generateKey();  
        byte[] bytesKey = secretKey.getEncoded();  

        // key转换  
        SecretKeySpec key = new SecretKeySpec(bytesKey, ALGORITHM);  
        
        // 加密  
        Cipher cipher = null;
        byte[] result = null;
        try {
            cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE, key);            
            result = cipher.doFinal(toEnc.getBytes());            
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            e.printStackTrace();  
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            e.printStackTrace();
        }        
        return Hex.encodeHexString(result);
    }
    
    public static String doDecrypt(String toDec, String seed){
        // 生成密钥  
        KeyGenerator keyGenerator = null;
        try {
            keyGenerator = KeyGenerator.getInstance(ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        System.out.println(keyGenerator.getProvider());  
        /*
         * 指定key的长度为128位
         * 给定种子作为随机数初始化出。若不指定种子信息,则秘钥生产器初始化将会完全随机,最后无法解密
         */
        SecureRandom random = null;
        try {
            random = SecureRandom.getInstance("SHA1PRNG");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        random.setSeed(seed.getBytes());
        keyGenerator.init(KEY_SIZE, random);         
        SecretKey secretKey = keyGenerator.generateKey();  
        byte[] bytesKey = secretKey.getEncoded();  

        // key转换  
        SecretKeySpec key = new SecretKeySpec(bytesKey, ALGORITHM);  
        // 解密  
        Cipher cipher = null;
        byte[] result = null;
        try {
            cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte rawHex[] = Hex.decodeHex(toDec);                        
            result = cipher.doFinal(rawHex);  
        } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
            e.printStackTrace();  
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException | BadPaddingException e) {
            e.printStackTrace();
        } catch (DecoderException e) {
            e.printStackTrace();
        }        
        return new String(result);                
    }
    
    public static void main(String args[]){
        String inPass = "二师兄到底是帅啊!";
        System.out.println("加密之前:" + inPass);
        String ouPass = JdkAesHelper.doEncrypt(inPass, SEED);
        System.out.println("加密之后:" + ouPass);
        String revPass = JdkAesHelper.doDecrypt(ouPass, SEED);
        System.out.println("解密之后:" + revPass);
    }

    /**
     * @return the sEED
     */
    protected static String getSEED() {
        return SEED;
    }
}

 

2. 接下来,就是PropertiesFactoryBean类的重写逻辑

package com.tk.robotbi.core.engine;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import javax.annotation.PostConstruct;

import org.springframework.beans.factory.config.PropertiesFactoryBean;

/**
 * @author shihuc
 * @date  2018年3月8日 上午8:58:23
 * 
 * 对系统配置参数,里面涉及到的加密了的信息,在实际使用之前,进行解密恢复明文使用,实现软件包静态传递的时候,不至于泄露重要的敏感信息。例如数据库的密码。
 * 注意:1. 此方法是基于PropertiesFactoryBean的扩展,方便配置文件里的参数,通过注解的方式在java文件中直接使用。
 *     2. 推荐使用此种方法。
 */
public class MyPropertiesFactoryBean extends PropertiesFactoryBean {
    
    /*
     * 需要解密的参数列表
     */
    private List<String> decryptParams;
        
    @PostConstruct
    public void init(){        
        if(decryptParams == null){
            try {
                throw new Exception("parameter container initialization error");
            } catch (Exception e) {            
                e.printStackTrace();
            }
        }        
    }
    
    /**
     * 重写属性加载函数mergeProperties(),在这个函数里面,将待解密的参数找出来,然后进行指定的解密函数操作。
     * 注意: 这里是静态的,默认都用一种解密算法进行,后期可以灵活的配置,实现不同的参数采用不同的解密算法,或者算法可以配置。
     */
    @Override
    protected Properties mergeProperties()    throws IOException {
        Properties result = super.mergeProperties();
        
        /*
         * 将配置过程中带入的信息首尾空格去掉,否则导致解密失败
         */
        List<String> params = new ArrayList<String>();
        for(String param: this.decryptParams){
            String newParam = param.trim();
            params.add(newParam);
        }
        
        Enumeration<?> keys = result.propertyNames();  
        while (keys.hasMoreElements()) {  
            String key = (String)keys.nextElement();  
            String value = result.getProperty(key);  
            if (params.contains(key) && null != value) {  
                result.remove(key);
                value = JdkAesHelper.doDecrypt(value.trim(),JdkAesHelper.getSEED());  
                result.setProperty(key, value);  
            } 
        } 
        return result;
    }

    /**
     * @return the decryptParams
     */
    public List<String> getDecryptParams() {
        return decryptParams;
    }

    /**
     * @param params the decryptParams to set
     */
    public void setDecryptParams(List<String> params) {
        this.decryptParams = params;        
    }
}

 

3. 最后,就是spring的配置文件的修改

默认的基于PropertiesFactoryBean的配置信息如下:

<bean id="configRealm" class="org.springframework.beans.factory.config.PropertiesFactoryBean">    
    <property name="locations">
        <list>
            <value>classpath:conf/jdbc.properties</value>
            <value>classpath:conf/mongo.properties</value>
            <value>classpath:conf/redis.properties</value>                
            <value>classpath:conf/params.properties</value>
            <value>classpath:conf/elasticsearch.properties</value>
            <value>classpath:conf/fileUpload.properties</value>
        </list>
    </property>
</bean>

改用重写后的PropertiesFactoryBean的配置信息如下:

<bean id="configRealm" class="com.tk.robotbi.core.engine.MyPropertiesFactoryBean">
    <property name="decryptParams">
        <list>
            <value>mongo.write1.password </value>
            <value>mongo.read1.password </value>
            <value>mongo.write2.password</value>
            <value>mongo.read2.password</value>
        </list>
    </property>
    <property name="locations">
        <list>
            <value>classpath:conf/jdbc.properties</value>
            <value>classpath:conf/mongo.properties</value>
            <value>classpath:conf/redis.properties</value>                
            <value>classpath:conf/params.properties</value>
            <value>classpath:conf/elasticsearch.properties</value>
            <value>classpath:conf/fileUpload.properties</value>
        </list>
    </property>
</bean>

 

基于上述几步之后,就需要将配置文件里面出现在decryptParams列表的参数用加密后的信息写入相应的配置文件,这里,我们将mongo数据库的密码进行加密写入mongo.properties,有两组的读写库密码。运行程序,启动正常,数据能够连接上。一切ok。

 

这里,需要注意的有下面几点:

1. spring的配置文件中,参数值后面或者前面的空格,spring加载参数的时候,是不会给trim掉的,必须自己在应用逻辑中处理。

2. spring的配置文件中,bean的list类型的成员变量,类型不同,配置方式不同。

基本类型的,可以采用如下的形式配置:
<list>
  <value>aaa</value>
  <value>bbb</value>
  <value>ccc</value>
</list>
若bean的list类型成员变量,不是基本类型,而是具体的bean,那么需要采用下面的形式配置:
<list>
  <ref bean="bean1"/>
  <ref bean="bean2"/>
  <ref bean="bean3"/>
</list>

3. spring的配置文件中,list的类型的参数配置,在bean的定义中,成员变量,可以定义成数组,也可以定义成List。

例如本加解密案例中,MyPropertiesFactoryBean的实现过程中,private List<String> decryptParams;的定义,可以改成private String[] decryptParams;效果是一样的。只要注意响应的setter和getter方法做适当的调整即可。


4. 继承PropertiesFactoryBean,重写函数mergeProperties(),与继承org.springframework.beans.factory.config.PropertyPlaceholderConfigurer然后重写processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)都可以达到类似的效果。只是建议采用继承PropertiesFactoryBean的方式,方便参数通过注解的形式在java程序中直接引用参数值。
不管是继承PropertiesFactoryBean的方案还是继承PropertyPlaceholderConfigurer的方案,其实最终关注到的核心都是来自共同的父类PropertiesLoaderSupport中的properties的加载逻辑前后的处理。都可以通过修改mergeProperties()函数的逻辑实现。当然也可以按照各自的需要进行调整相应函数的重写逻辑。

PS:附上继承org.springframework.beans.factory.config.PropertyPlaceholderConfigurer然后重写processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props)的实现逻辑:

package com.tk.robotbi.core.engine;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import javax.annotation.PostConstruct;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;

/**
 * @author shihuc
 * @date  2018年3月8日 下午1:01:22
 * 
 * 对系统配置参数,里面涉及到的加密了的信息,在实际使用之前,进行解密恢复明文使用,实现软件包静态传递的时候,不至于泄露重要的敏感信息。例如数据库的密码。
 * 注意:1. 此种方法是对PropertyPlaceholderConfigurer的扩展,实现思路直接。
 *     2. 较常用,但是不便于配置文件中的参数通过注解的方式在java文件中直接使用。
 * 
 */
public class MyPropertiesPlaceholderConfigurer extends PropertyPlaceholderConfigurer {

    /*
     * 需要解密的参数列表
     */
    private List<String> decryptParams;
    
    @PostConstruct
    public void init(){
        if(decryptParams == null){
            try {
                throw new Exception("parameter container initialization error");
            } catch (Exception e) {            
                e.printStackTrace();
            }
        }
    }
    
    
    /**
     * 重写属性处理函数,在这个函数里面,将待解密的参数找出来,然后进行指定的解密函数操作。
     * 注意: 这里是静态的,默认都用一种解密算法进行,后期可以灵活的配置,实现不同的参数采用不同的解密算法,或者算法可以配置。
     */
    protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException {        
        Enumeration<?> keys = props.propertyNames(); 
        
        /*
         * 将配置过程中带入的信息首尾空格去掉,否则导致解密失败
         */
        List<String> params = new ArrayList<String>();
        for(String param: this.decryptParams){
            String newParam = param.trim();
            params.add(newParam);
        }
        
        while (keys.hasMoreElements()) {  
            String key = (String)keys.nextElement();  
            String value = props.getProperty(key);  
            if (params.contains(key) && null != value) {  
                props.remove(key);
                value = JdkAesHelper.doDecrypt(value.trim(),JdkAesHelper.getSEED());  
                props.setProperty(key, value);  
            }  
            System.setProperty(key, value);  
        }  
        super.processProperties(beanFactoryToProcess, props);
    }


    /**
     * @return the decryptParams
     */
    public List<String> getDecryptParams() {
        return decryptParams;
    }


    /**
     * @param decryptParams the decryptParams to set
     */
    public void setDecryptParams(List<String> decryptParams) {
        this.decryptParams = decryptParams;
    }
    
}