在Spring中如何使用加密外部屬性文件

在Spring的開發中,我們在很多情況下會使用佔位符引用屬性文件的屬性值來簡化我們的系統及使我們的系統具有更高的靈活性和通用性。這種配置方式有兩個明顯的好處:
?- 減少維護的工作量:資源的配置信息可以多應用共享,在多個應用使用同一資源的情況下,如果資源的地址、用戶名等配置信息發生了更改,你只要調整屬性文件就可以了;
?- 使部署更簡單:Spring配置文件主要描述應用程序中的Bean,這些配置信息在開發完成後,應該就固定下來了,在部署應用時,需要根據部署環境調整是就是數據源,郵件服務器的配置信息,將它們的配置信息獨立到屬性文件中,應用部署人員只需要調整資源屬性文件即可,根本不需要關注內容複雜的Spring 配置文件。不僅給部署和維護帶來了方便,也降低了出錯的機率。
Spring爲我們提供了一個BeanFactoryPostProcessorBean工廠後置處理器接口的實現類:org.springframework.beans.factory.config.PropertyPlaceholderConfigurer,它的主要功能是對引用了外部屬性值的<bean>進行處理,將其翻譯成真實的配置值。
一般的屬性信息以明文的方式存放在屬性文件中並沒有什麼問題,但如果是數據源或郵件服務器用戶名密碼等重要的信息,在某些場合,我們可能需要以密文的方式保存。雖然Web應用的客戶端用戶看不到配置文件的,但有時,我們只希望特定的維護人員掌握重要資源的配置信息,而不是毫無保留地對所有可以進入部署機器的用戶開放。
對於這種具有高度安全性要求的系統(如電信、銀行、重點人口庫等),我們需要對資源連接等屬性配置文件中的配置信息加密存放。然後讓Spring容器啓動時,讀入配置文件後,先進行解密,然後再進行佔位符的替換。
很可惜,PropertyPlaceholderConfigurer只支持明文的屬性文件。但是,我們可以充分利用Spring框架的擴展性,通過擴展 PropertyPlaceholderConfigurer類來達到我們的要求。本文將講解使用加密屬性文件的原理並提供具體的實現。

以傳統的方式使用屬性文件
一般情況下,外部屬性文件用於定義諸如數據源或郵件服務器之類的配置信息。這裏,我們通過一個簡單的例子,講解使用屬性文件的方法。假設有一個car.properties屬性文件,文件內容如下:
brand=紅旗CA72 
maxSpeed=250
price=20000.00

該文件放在類路徑的com/baobaotao/目錄下,在Spring配置文件中利用PropertyPlaceholderConfigurer引入這個配置文件,並通過佔位符引用屬性文件內的屬性項,如代碼清單 1所示:
代碼清單 1 使用外部屬性文件進行配置
Java代碼
<!-- ① 引入外部屬性文件 -->  
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:com/baobaotao/car.properties</value> ② 指定屬性文件地址
</list>
</property>
<property name="fileEncoding" value="utf-8"/>
</bean>
<!-- ③ 引用外部屬性的值,對car進行配置 -->
<bean id="car" class="com.baobaotao.place.Car">
<property name="brand" value="${brand}" />
<property name="maxSpeed" value="${maxSpeed}" />
<property name="price" value="${price}" />
</bean>
<!-- ① 引入外部屬性文件 -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:com/baobaotao/car.properties</value> ② 指定屬性文件地址
</list>
</property>
<property name="fileEncoding" value="utf-8"/>
</bean>
<!-- ③ 引用外部屬性的值,對car進行配置 -->
<bean id="car" class="com.baobaotao.place.Car">
<property name="brand" value="${brand}" />
<property name="maxSpeed" value="${maxSpeed}" />
<property name="price" value="${price}" />
</bean>

在①處,我們通過PropertyPlaceholderConfigurer這個BeanFactoryPostProcessor實現類引用外部的屬性文件,通過它的locations屬性指定Spring配置文件中引用到的屬性文件,在PropertyPlaceholderConfigurer內部,locations是一個Resource數組,所以你可以在地址前添加資源類型前綴,如②處所示。如果需要引用多個屬性文件,只需要在②處添加相應<value>配置項即可。

分析PropertyPlaceholderConfigurer結構
我們知道Spring通過PropertyPlaceholderConfigurer提供對外部屬性文件的支持,爲了使用加密的屬性文件,我們就需要分析該類的工作機理,再進行改造。所以我們先來了解一下該類的結構(見附件1)。
其中PropertiesLoaderSupport類有一個重要的protected void loadProperties(Properties props)方法,查看它的註釋,可以知道該方法的作用是將PropertyPlaceholderConfigurer 中locations屬性所定義的屬性文件的內容讀取到props入參對象中。這個方法比較怪,Java很少通過入參承載返回值,但這個方法就是這樣。

所以,我們只要簡單地重載這個方法,在將資源文件的內容轉換爲Properties之前,添加一個解密的步驟就可以了。但是,PropertiesLoaderSupport的設計有一個很讓人遺憾的地方,它的locations屬性是private的,只提供setter 沒有提供getter。因此,無法在子類中獲取PropertiesLoaderSupport中的locations(資源地址),所以我們得在子類重新定義locations屬性並覆蓋PropertiesLoaderSupport中的setLocations()方法。
編寫支持加密屬性文件的實現類
通過以上分析,我們設計一個支持加密屬性文件的增強型PropertyPlaceholderConfigurer,其代碼如所示:
代碼清單 2
Java代碼
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();
}
}
}
}
}
}

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加密的功能,因爲屬性文件在部署前必須經常加密:
代碼清單 3 加密解密工具類

Java代碼
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容器支持加密的屬性文件:

Java代碼
<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>
<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,在屬性文件流加載後應用前進行解密就可以很好地解決這個問題了。
1:
java org/evolve/test/encryptor/DESEncryptUtil key jdbc.dat
成功生成密鑰文件。
2:
java org/evolve/test/encryptor/DESEncryptUtil encrypt jdbc.properties jdbc.dat
成功加密,加密文件位於:/home/yaoyu/workspace/myweb/WebContent/WEB-INF/classes/en_jdbc.properties
3:
java org/evolve/test/encryptor/DESEncryptUtil decrypt en_jdbc.properties jdbc.dat
成功解密,解密文件位於:/home/yaoyu/workspace/myweb/WebContent/WEB-INF/classes/rs_en_jdbc.properties
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章