当我们在使用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。