基于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; } }