當(dāng)我們?cè)谑褂肧pring進(jìn)行開(kāi)發(fā)時(shí),我們經(jīng)常使用占位符引用屬性文件的屬性值來(lái)簡(jiǎn)化我們的配置及使我們的配置具有更高的靈活性和通用性。
使用這種方式的好處這里就不贅述了,這里要講的是怎樣對(duì)此外部屬性文件的屬性值進(jìn)行加密、解密。
以下是我們熟悉的配置:jdbc.properties
driver=oracle.jdbc.OracleDriver
dburl=jdbc:oracle:thin:@127.0.0.1:1521:root
username=myusr
password=mypassword
applicationContext.xml
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:jdbc.properties</value>
</list>
</property>
<property name="fileEncoding" value="utf-8"/>
</bean>
<bean id="proxoolDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}" />
<property name="url" value="${dburl}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</bean>
但當(dāng)我們配置一個(gè)應(yīng)用的時(shí)候,假設(shè)打成myapp.jar文件,并不希望將一些配置文件一同打在jar里。
原因是:
有的配置文件要經(jīng)常改動(dòng),例如:由于環(huán)境的不同,數(shù)據(jù)庫(kù)的連接信息要經(jīng)常變動(dòng),出于安全方面的考慮,密碼要經(jīng)常變換。
一旦配置文件有了修改,就要停止myapp.jar程序,重新打包,再啟動(dòng)myapp.jar,這樣無(wú)疑對(duì)于維護(hù)人員來(lái)說(shuō)是很杯具的。
如果我們將配置文件放在myapp.jar外面,那么每次修改配置文件后,只要重啟myapp.jar即可。
在配置一些敏感屬性的時(shí)候(例如密碼等),需要對(duì)其進(jìn)行加密。
我們期望看到的jdbc.properties的內(nèi)容是這樣的:
jdbc.properties
driver=oracle.jdbc.OracleDriver
dburl=jdbc:oracle:thin:@127.0.0.1:1521:root
username=myusr
password={3DES}VwHsU01hJOqskgCppbmTXg==
對(duì)password屬性值進(jìn)行3DES加密(這里提供了對(duì)加密方式的配置),其他的屬性值不變。
既達(dá)到了安全的效果,又讓配置清晰明了。
好了,讓我們開(kāi)始來(lái)實(shí)現(xiàn)我們的需求吧。
我們從org.springframework.beans.factory.config.PropertyPlaceholderConfigurer這個(gè)類入手。
因?yàn)橹拔覀兌际怯眠@個(gè)類來(lái)完成對(duì)外部屬性文件的引用的。
讀了一下這個(gè)類的代碼,沒(méi)發(fā)現(xiàn)能入手的地方,繼續(xù)找它的父類。
最終,PropertiesLoaderSupport.java 這個(gè)抽象類被我們發(fā)現(xiàn)了。其中的loadProperties方法便是我們的入口。
看此方法的注釋大意是:加載屬性到已給出的實(shí)例(翻譯的很白癡,汗)。
原來(lái)Spring先是生成一個(gè)Properties的實(shí)例,然后通過(guò)這個(gè)loadProperties方法,將屬性的鍵值對(duì)設(shè)置到該實(shí)例中。該實(shí)例相當(dāng)于一個(gè)籃子,進(jìn)入方法時(shí),是一個(gè)空籃子,待方法返回時(shí),將籃子裝滿。
以下請(qǐng)看代碼,對(duì)該段代碼進(jìn)行簡(jiǎn)單理解:

/** *//**
* Load properties into the given instance.
* @param props the Properties instance to load into
* @throws java.io.IOException in case of I/O errors
* @see #setLocations
*/

protected void loadProperties(Properties props) throws IOException
{

if (this.locations != null)
{

for (int i = 0; i < this.locations.length; i++)
{ // 遍歷屬性文件列表
Resource location = this.locations[i]; // 取得一個(gè)屬性文件句柄

if (logger.isInfoEnabled())
{
logger.info("Loading properties file from " + location);
}
InputStream is = null;

try
{
is = location.getInputStream();

if (location.getFilename().endsWith(XML_FILE_EXTENSION))
{ // 判斷該屬性文件是否為.xml文件
this.propertiesPersister.loadFromXml(props, is); // 此處略過(guò),我們只考慮.properties文件
}

else
{

if (this.fileEncoding != null)
{ // 加載屬性文件 入口1
this.propertiesPersister.load(props, new InputStreamReader(is, this.fileEncoding));
}

else
{ // 加載屬性文件 入口2
this.propertiesPersister.load(props, is);
}
}
}

catch (IOException ex)
{

if (this.ignoreResourceNotFound)
{

if (logger.isWarnEnabled())
{
logger.warn("Could not load properties from " + location + ": " + ex.getMessage());
}
}

else
{
throw ex;
}
}

finally
{

if (is != null)
{
is.close();
}
}
}
}
}
在入口1、入口2處實(shí)現(xiàn)load接口的是org.springframework.util.DefaultPropertiesPersister.java。
分別看一下這兩個(gè)方法:
1.void load(Properties props, Reader reader) // 入口1分支
2.load(Properties props, InputStream is) // 入口2分支
先看入口2的方法吧,因?yàn)樗鼘⑹潜晃覀兲蕴姆椒āT蚴撬贿m合我們改造
以下是入口load方法的實(shí)現(xiàn):

public void load(Properties props, InputStream is) throws IOException
{
props.load(is); // props為java.util.Properties對(duì)象。
}
props為java.util.Properties對(duì)象,所以想要在這里做文章會(huì)比較麻煩。
所以我選擇入口1.

public void load(Properties props, Reader reader) throws IOException
{

if (loadFromReaderAvailable)
{
// On JDK 1.6+
props.load(reader); // 入口3
}

else
{
// Fall back to manual parsing.
doLoad(props, reader); // 入口4
}
}
入口3也被放棄,理由同放棄入口2。
讓我們看看入口4的具體實(shí)現(xiàn):

protected void doLoad(Properties props, Reader reader) throws IOException
{
BufferedReader in = new BufferedReader(reader);

while (true)
{
String line = in.readLine();

if (line == null)
{
return;
}
line = StringUtils.trimLeadingWhitespace(line);

if (line.length() > 0)
{
char firstChar = line.charAt(0);

if (firstChar != '#' && firstChar != '!')
{

while (endsWithContinuationMarker(line))
{
String nextLine = in.readLine();
line = line.substring(0, line.length() - 1);

if (nextLine != null)
{
line += StringUtils.trimLeadingWhitespace(nextLine);
}
}
int separatorIndex = line.indexOf("=");

if (separatorIndex == -1)
{
separatorIndex = line.indexOf(":");
}
// 從這里開(kāi)始便是我們要改造的地方了。
// 得到value后,我們將value進(jìn)行解密,然后再裝到props這個(gè)籃子里。
String key = (separatorIndex != -1 ? line.substring(0, separatorIndex) : line);
String value = (separatorIndex != -1) ? line.substring(separatorIndex + 1) : "";
key = StringUtils.trimTrailingWhitespace(key);
value = StringUtils.trimLeadingWhitespace(value);
props.put(unescape(key), unescape(value));
}
}
}
}
知道了要改造的地方,那么我們寫(xiě)代碼吧。
新建類:DecryptPropertyPlaceholderConfigurer.java繼承 PropertyPlaceholderConfigurer.java
新建類:DecryptPropertiesPersister.java繼承DefaultPropertiesPersister.java
由于
locations、propertiesPersister、fileEncoding、ignoreResourceNotFound 這些變量在抽象類PropertiesLoaderSupport.java中并沒(méi)有提供set方法。
所以我們?cè)贒ecryptPropertyPlaceholderConfigurer.java聲明這些成員變量并將父類的覆蓋。
其中propertiesPersister變量用我們寫(xiě)的DefaultPropertiesPersister類來(lái)實(shí)現(xiàn)。
具體代碼:
DecryptPropertyPlaceholderConfigurer.java
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Properties;

import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.core.io.Resource;


public class DecryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer
{
private Resource[] locations;
private DecryptPropertiesPersister propertiesPersister = new DecryptPropertiesPersister();
private String fileEncoding = "utf-8";
private boolean ignoreResourceNotFound = false;

@Override

public void setLocations(Resource[] locations)
{
this.locations = locations;
}

@Override

public void setFileEncoding(String encoding)
{
this.fileEncoding = encoding;
}

@Override

public void setIgnoreResourceNotFound(boolean ignoreResourceNotFound)
{
this.ignoreResourceNotFound = ignoreResourceNotFound;
}

@Override

public void loadProperties(Properties props) throws IOException
{

if (this.locations != null)
{

for (int i = 0; i < this.locations.length; i++)
{
Resource location = this.locations[i];
InputStream is = null;

try
{
is = location.getInputStream();

if (location.getFilename().endsWith(XML_FILE_EXTENSION))
{
this.propertiesPersister.loadFromXml(props, is);

} else
{
this.propertiesPersister.doLoad(props, new InputStreamReader(is,
this.fileEncoding));
}

} catch (IOException ex)
{

if (this.ignoreResourceNotFound)
{

if (logger.isWarnEnabled())
{
logger.warn("Could not load properties from " + location + ": "
+ ex.getMessage());
}

} else
{
throw ex;
}

} finally
{

if (is != null)
{
is.close();
}
}
}
}
}
}
DecryptPropertiesPersister.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.Properties;

import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.StringUtils;


public class DecryptPropertiesPersister extends DefaultPropertiesPersister
{
@Override

protected void doLoad(Properties props, Reader reader) throws IOException
{
BufferedReader in = new BufferedReader(reader);

while (true)
{
String line = in.readLine();

if (line == null)
{
return;
}
line = StringUtils.trimLeadingWhitespace(line);

if (line.length() == 0)
{
continue;
}
char firstChar = line.charAt(0);

if (firstChar != '#' && firstChar != '!')
{

while (endsWithContinuationMarker(line))
{
String nextLine = in.readLine();
line = line.substring(0, line.length() - 1);

if (nextLine != null)
{
line += StringUtils.trimLeadingWhitespace(nextLine);
}
}
int separatorIndex = line.indexOf("=");

if (separatorIndex == -1)
{
separatorIndex = line.indexOf(":");
}
String key = (separatorIndex != -1 ? line.substring(0, separatorIndex) : line);
String value = (separatorIndex != -1) ? line.substring(separatorIndex + 1) : "";
key = StringUtils.trimTrailingWhitespace(key);

// 從這里開(kāi)始,我們要關(guān)注了。
value = StringUtils.trimLeadingWhitespace(value);
// 對(duì)加密的屬性進(jìn)行3DES解密
value = decrypt("key", value);// 解密方法略 密鑰配置的方法略
props.put(unescape(key), unescape(value));
}
}
}

private String decrypt(String key, String str)
{

if(org.apache.commons.lang.StringUtils.isEmpty(str))
{
return "";
}
// 解密方法

if(key.startsWith("{3DES}"))
{
// 解密 略
}
return str;
}
}
修改applicationContext.xml文件如下:
<bean id="propertyConfigurer"
class="com.jn.spring.DecryptPropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:jdbc.properties</value>
</list>
</property>
<property name="fileEncoding" value="utf-8"/>
</bean>
<bean id="proxoolDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}" />
<property name="url" value="${dburl}" />
<property name="username" value="${username}" />
<property name="password" value="${password}" />
</bean>
這樣就完成了。在Spring進(jìn)行加載的時(shí)候,debug看看是否解析對(duì)了就OK了。
注:由于這里主要講解的是如果通過(guò)擴(kuò)展Spring而實(shí)現(xiàn)對(duì)外部屬性文件的屬性值進(jìn)行加密,而不是介紹加密解密方法,所以關(guān)于加密解密方法略。
google一下到處都是。
本文為原創(chuàng),歡迎轉(zhuǎn)載,轉(zhuǎn)載請(qǐng)注明出處BlogJava。