扩展Spring——外部属性文件安全(二)

编写支持加密属性文件的实现类

通过以上分析,我们设计一个支持加密属性文件的增强型PropertyPlaceholderConfigurer,其代码如所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.Key;
import java.util.Properties;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.core.io.Resource;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
public class DecryptPropertyPlaceholderConfigurer
extends PropertyPlaceholderConfigurer ...{
private Resource[] locations; //① 重新定义父类中的这个同名属性
private Resource keyLocation; //② 用于指定密钥文件
public void setKeyLocation(Resource keyLocation) ...{
this.keyLocation = keyLocation;
}
public void setLocations(Resource[] locations) ...{
this.locations = locations;
}
public void loadProperties(Properties props) throws IOException ...{
if (this.locations != null) ...{
PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
for (int i = 0; i < this.locations.length; i++) ...{
Resource location = this.locations[i];
if (logger.isInfoEnabled()) ...{
logger.info("Loading properties file from " + location);
}
InputStream is = null;
try ...{
is = location.getInputStream();
//③ 加载密钥
Key key = DESEncryptUtil.getKey(keyLocation.getInputStream());
//④ 对属性文件进行解密
is = DESEncryptUtil.doDecrypt(key, is);
//⑤ 将解密后的属性流装载到props中
if(fileEncoding != null)...{
propertiesPersister.load(props,
new InputStreamReader(is,fileEncoding));
}else...{
propertiesPersister.load(props ,is);
}
} finally ...{
if (is != null)
is.close();
}
}
}
}
}
}

对locations指定的属性文件流数据进行额外的解密工作,解密后再装载到props中。比起PropertyPlaceholderConfigurer,我们只做了额外的一件事:装载前对属性资源进行解密。

在代码清单 2的③和④处,我们使用了一个DES解密的工具类对加密的属性文件流进行解密。 对文件进行对称加密的算法很多,一般使用DES对称加密算法,因为它速度很快,破解困难,DESEncryptUtil不但提供了DES解密功能,还提供了DES加密的功能,因为属性文件在部署前必须经常加密:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
public class DESEncryptUtil ...{
public static Key createKey() throws NoSuchAlgorithmException {//创建一个密钥
Security.insertProviderAt(new com.sun.crypto.provider.SunJCE(), 1);
KeyGenerator generator = KeyGenerator.getInstance("DES");
generator.init(new SecureRandom());
Key key = generator.generateKey();
return key;
}
public static Key getKey(InputStream is) {
try ...{
ObjectInputStream ois = new ObjectInputStream(is);
return (Key) ois.readObject();
} catch (Exception e) ...{
e.printStackTrace();
throw new RuntimeException(e);
}
}
private static byte[] doEncrypt(Key key, byte[] data) {//对数据进行加密
try {
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] raw = cipher.doFinal(data);
return raw;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static InputStream doDecrypt(Key key, InputStream in) {//对数据进行解密
try {
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, key);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] tmpbuf = new byte[1024];
int count = 0;
while ((count = in.read(tmpbuf)) != -1) {
bout.write(tmpbuf, 0, count);
tmpbuf = new byte[1024];
}
in.close();
byte[] orgData = bout.toByteArray();
byte[] raw = cipher.doFinal(orgData);
ByteArrayInputStream bin = new ByteArrayInputStream(raw);
return bin;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
public static void main(String[] args) throws Exception {//提供了Java命令使用该工具的功能
if (args.length == 2 && args[0].equals("key")) {// 生成密钥文件
Key key = DESEncryptUtil.createKey();
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(args[1]));
oos.writeObject(key);
oos.close();
System.out.println("成功生成密钥文件。");
} else if (args.length == 3 && args[0].equals("encrypt")) {//对文件进行加密
File file = new File(args[1]);
FileInputStream in = new FileInputStream(file);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] tmpbuf = new byte[1024];
int count = 0;
while ((count = in.read(tmpbuf)) != -1) {
bout.write(tmpbuf, 0, count);
tmpbuf = new byte[1024];
}
in.close();
byte[] orgData = bout.toByteArray();
Key key = getKey(new FileInputStream(args[2]));
byte[] raw = DESEncryptUtil.doEncrypt(key, orgData);
file = new File(file.getParent() + "\\en_" + file.getName());
FileOutputStream out = new FileOutputStream(file);
out.write(raw);
out.close();
System.out.println("成功加密,加密文件位于:"+file.getAbsolutePath());
} else if (args.length == 3 && args[0].equals("decrypt")) {//对文件进行解密
File file = new File(args[1]);
FileInputStream fis = new FileInputStream(file);
Key key = getKey(new FileInputStream(args[2]));
InputStream raw = DESEncryptUtil.doDecrypt(key, fis);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
byte[] tmpbuf = new byte[1024];
int count = 0;
while ((count = raw.read(tmpbuf)) != -1) {
bout.write(tmpbuf, 0, count);
tmpbuf = new byte[1024];
}
raw.close();
byte[] orgData = bout.toByteArray();
file = new File(file.getParent() + "\\rs_" + file.getName());
FileOutputStream fos = new FileOutputStream(file);
fos.write(orgData);
System.out.println("成功解密,解密文件位于:"+file.getAbsolutePath());
}
}
}

解密工作主要涉及到两个类Cipher和Key,前者是加密器,可以通过init()方法设置工作模式和密钥,在这里,我们设置为解密工作模式:Cipher.DECRYPT_MODE。Cipher通过doFinal()方法对字节数组进行加密或解密。

要完成属性文件的加密工作,首先,必须获取一个密钥文件,然后才能对明文的属性文件进行加密。如果需要调整属性文件的信息,你必须执行相反的过程,即用密钥对加密后的属性文件进行解密,调整属性信息后,再将其加密。
DESEncryptUtil 工具类可以完成以上所提及的三个工作:

  • 生成一个密钥文件
    java com.baobaotao.DESEncryptUtil key D:\key.dat
    第一个参数为key,表示创建密钥文件,第二个参数为生成密钥文件的保存地址。
  • 用密钥文件对属性文件进行加密
    java com.baobaotao.DESEncryptUtil encrypt d:\test.properties d:\key.dat
    第一个参数为encrypt,表示加密,第二个参数为需要加密的属性文件,第三个参数为密钥文件。如果加密成功,将生成en_test.properties的加密文件。
  • 用密钥文件对加密后的属性文件进行解密
    java com.baobaotao.DESEncryptUtil decrypt d:\test.properties d:\key.dat
    第一个参数为decrypt,表示解密,第二个参数为需要解密的属性文件,第三个参数为密钥文件。如果加密成功,将生成rs_test.properties的解密文件。

在Spring中配置加密属性文件

假设我们通过DESEncryptUtil 工具类创建了一个key.bat密钥,并对 car.properties 属性进行加密,生成加密文件en_car.properties。下面,我们通过DecryptPropertyPlaceholderConfigurer增强类进行配置,让Spring容器支持加密的属性文件:

假设我们通过DESEncryptUtil 工具类创建了一个key.bat密钥,并对car.properties属性进行加密,生成加密文件en_car.properties。下面,我们通过DecryptPropertyPlaceholderConfigurer增强类进行配置,让Spring容器支持加密的属性文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean class="com.baobaotao.place.DecryptPropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:com/baobaotao/en_car.properties</value>
</list>
</property>
<property name="keyLocation" value="classpath:com/baobaotao/key.dat" />
<property name="fileEncoding" value="utf-8" />
</bean>
<bean id="car" class="com.baobaotao.place.Car">
<property name="brand" value="${brand}" />
<property name="maxSpeed" value="${maxSpeed}" />
<property name="price" value="${price}" />
</bean>

注意①处的配置,我们使用自己编写的DecryptPropertyPlaceholderConfigurer替代Spring的PropertyPlaceholderConfigurer,由于前者对属性文件进行了特殊的解密处理,因此②处的car Bean也可以引用到加密文件en_car.properties中的属性项。

小结

要Spring配置时,将一些重要的信息独立到属性文件中是比较常见的做法,Spring只支持明文存放的属性文件,在某些场合下,我们可以希望对属性文件加密保存,以保证关键信息的安全。通过扩展PropertyPlaceholderConfigurer,在属性文件流加载后应用前进行解密就可以很好地解决这个问题了。