基于spring的placeholder思路处理配置信息敏感信息加密解密的实践

时间:2022-01-07 12:38:33

基于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 = ; /*
* 这里的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;
} }