當我們在使用Spring進行開發時,我們經常使用占位符引用屬性文件的屬性值來簡化我們的配置及使我們的配置具有更高的靈活性和通用性。
使用這種方式的好處這里就不贅述了,這里要講的是怎樣對此外部屬性文件的屬性值進行加密、解密。
以下是我們熟悉的配置: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>
但當我們配置一個應用的時候,假設打成myapp.jar文件,并不希望將一些配置文件一同打在jar里。
原因是:
有的配置文件要經常改動,例如:由于環境的不同,數據庫的連接信息要經常變動,出于安全方面的考慮,密碼要經常變換。
一旦配置文件有了修改,就要停止myapp.jar程序,重新打包,再啟動myapp.jar,這樣無疑對于維護人員來說是很杯具的。
如果我們將配置文件放在myapp.jar外面,那么每次修改配置文件后,只要重啟myapp.jar即可。
在配置一些敏感屬性的時候(例如密碼等),需要對其進行加密。
我們期望看到的jdbc.properties的內容是這樣的:
jdbc.properties
driver=oracle.jdbc.OracleDriver
dburl=jdbc:oracle:thin:@127.0.0.1:1521:root
username=myusr
password={3DES}VwHsU01hJOqskgCppbmTXg==
對password屬性值進行3DES加密(這里提供了對加密方式的配置),其他的屬性值不變。
既達到了安全的效果,又讓配置清晰明了。
好了,讓我們開始來實現我們的需求吧。
我們從org.springframework.beans.factory.config.PropertyPlaceholderConfigurer這個類入手。
因為之前我們都是用這個類來完成對外部屬性文件的引用的。
讀了一下這個類的代碼,沒發現能入手的地方,繼續找它的父類。
最終,PropertiesLoaderSupport.java 這個抽象類被我們發現了。其中的loadProperties方法便是我們的入口。
看此方法的注釋大意是:加載屬性到已給出的實例(翻譯的很白癡,汗)。
原來Spring先是生成一個Properties的實例,然后通過這個loadProperties方法,將屬性的鍵值對設置到該實例中。該實例相當于一個籃子,進入方法時,是一個空籃子,待方法返回時,將籃子裝滿。
以下請看代碼,對該段代碼進行簡單理解:

/** *//**
* 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]; // 取得一個屬性文件句柄

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); // 此處略過,我們只考慮.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處實現load接口的是org.springframework.util.DefaultPropertiesPersister.java。
分別看一下這兩個方法:
1.void load(Properties props, Reader reader) // 入口1分支
2.load(Properties props, InputStream is) // 入口2分支
先看入口2的方法吧,因為它將是被我們淘汰的方法。原因是它不適合我們改造
以下是入口load方法的實現:

public void load(Properties props, InputStream is) throws IOException
{
props.load(is); // props為java.util.Properties對象。
}
props為java.util.Properties對象,所以想要在這里做文章會比較麻煩。
所以我選擇入口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的具體實現:

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(":");
}
// 從這里開始便是我們要改造的地方了。
// 得到value后,我們將value進行解密,然后再裝到props這個籃子里。
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));
}
}
}
}
知道了要改造的地方,那么我們寫代碼吧。
新建類:DecryptPropertyPlaceholderConfigurer.java繼承 PropertyPlaceholderConfigurer.java
新建類:DecryptPropertiesPersister.java繼承DefaultPropertiesPersister.java
由于
locations、propertiesPersister、fileEncoding、ignoreResourceNotFound 這些變量在抽象類PropertiesLoaderSupport.java中并沒有提供set方法。
所以我們在DecryptPropertyPlaceholderConfigurer.java聲明這些成員變量并將父類的覆蓋。
其中propertiesPersister變量用我們寫的DefaultPropertiesPersister類來實現。
具體代碼:
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);

// 從這里開始,我們要關注了。
value = StringUtils.trimLeadingWhitespace(value);
// 對加密的屬性進行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進行加載的時候,debug看看是否解析對了就OK了。
注:由于這里主要講解的是如果通過擴展Spring而實現對外部屬性文件的屬性值進行加密,而不是介紹加密解密方法,所以關于加密解密方法略。
google一下到處都是。
本文為原創,歡迎轉載,轉載請注明出處BlogJava。