等占位符引用屬性檔案的屬性值,這種配置方式有兩個明顯的好處:
- 減少維護的工作量:資源的配置資訊可以多應用共享,在多個應用使用同一資源的情況下,如果資源的位址、使用者名等配置資訊發生了更改,你隻要調整屬性檔案就可以了;
- 使部署更簡單: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 使用外部屬性檔案進行配置
① 引入外部屬性檔案
<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提供對外部屬性檔案的支援,為了使用加密的屬性檔案,我們就需要分析該類的工作機理,再進行改造。是以我們先來了解一下該類的結構:
其中PropertiesLoaderSupport類有一個重要的protected void loadProperties(Properties props)方法,檢視它的注釋,可以知道該方法的作用是将PropertyPlaceholderConfigurer 中locations屬性所定義的屬性檔案的内容讀取到props入參對象中。這個方法比較怪,Java很少通過入參承載傳回值,但這個方法就是這樣。
是以,我們隻要簡單地重載這個方法,在将資源檔案的内容轉換為Properties之前,添加一個解密的步驟就可以了。但是,PropertiesLoaderSupport的設計有一個很讓人遺憾的地方,它的locations屬性是private的,隻提供setter沒有提供getter。是以,無法在子類中擷取PropertiesLoaderSupport中的locations(資源位址),是以我們得在子類重新定義locations屬性并覆寫PropertiesLoaderSupport中的setLocations()方法。
編寫支援加密屬性檔案的實作類
通過以上分析,我們設計一個支援加密屬性檔案的增強型PropertyPlaceholderConfigurer,其代碼如所示:
代碼清單 2 DecryptPropertyPlaceholderConfigurer
package com.baobaotao;
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解密的工具類對加密的屬性檔案流進行解密。
加密解密工具類DESEncryptUtil
對檔案進行對稱加密的算法很多,一般使用DES對稱加密算法,因為它速度很快,破解困難,DESEncryptUtil不但提供了DES解密功能,還提供了DES加密的功能,因為屬性檔案在部署前必須經常加密:
圖 2 加密解密工具類
package com.baobaotao.place;
…
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容器支援加密的屬性檔案:
<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,在屬性檔案流加載後應用前進行解密就可以很好地解決這個問題了。