`

扩展PropertyPlaceholderConfigurer对prop文件中的属性加密

 
阅读更多

转载:http://my.oschina.net/noahxiao/blog/163719

一、背景

处于安全考虑需要对.properties中的数据库用户名与密码等敏感数据进行加密。项目中使用了Spring3框架统一加载属性文件,所以最好可以干扰这个加载过程来实现对.properties文件中的部分属性进行加密。

属性文件中的属性最初始时敏感属性值可以为明文,程序第一次执行后自动加密明文为密文。

二、问题分析

  1. 扩展PropertyPlaceholderConfigurer最好的方式就是编写一个继承该类的子类。
  2. 外部设置locations时,记录全部locations信息,为加密文件保留属性文件列表。重写setLocations与setLocation方法(在父类中locations私有)
  3. 寻找一个读取属性文件属性的环节,检测敏感属性加密情况。对有已有加密特征的敏感属性进行解密。重写convertProperty方法来实现。
  4. 属性文件第一次加载完毕后,立即对属性文件中的明文信息进行加密。重写postProcessBeanFactory方式来实现。

三、程序开发

1、目录结构

注:aes包中为AES加密工具类,可以根据加密习惯自行修改

2、EncryptPropertyPlaceholderConfigurer(详见注释)

001 package org.noahx.spring.propencrypt;
002  
003 import org.noahx.spring.propencrypt.aes.AesUtils;
004 import org.slf4j.Logger;
005 import org.slf4j.LoggerFactory;
006 import org.springframework.beans.BeansException;
007 import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
008 import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
009 import org.springframework.core.io.Resource;
010  
011 import java.io.*;
012 import java.util.*;
013 import java.util.regex.Matcher;
014 import java.util.regex.Pattern;
015  
016 /**
017  * Created with IntelliJ IDEA.
018  * User: noah
019  * Date: 9/16/13
020  * Time: 10:36 AM
021  * To change this template use File | Settings | File Templates.
022  */
023 public class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
024  
025     private static final String SEC_KEY = "@^_^123aBcZ*";    //主密钥
026     private static final String ENCRYPTED_PREFIX = "Encrypted:{";
027     private static final String ENCRYPTED_SUFFIX = "}";
028     private static Pattern encryptedPattern = Pattern.compile("Encrypted:\\{((\\w|\\-)*)\\}");  //加密属性特征正则
029  
030     private Logger logger = LoggerFactory.getLogger(this.getClass());
031  
032     private Set<String> encryptedProps = Collections.emptySet();
033  
034     public void setEncryptedProps(Set<String> encryptedProps) {
035         this.encryptedProps = encryptedProps;
036     }
037  
038     @Override
039     protected String convertProperty(String propertyName, String propertyValue) {
040  
041         if (encryptedProps.contains(propertyName)) { //如果在加密属性名单中发现该属性
042             final Matcher matcher = encryptedPattern.matcher(propertyValue);  //判断该属性是否已经加密
043             if (matcher.matches()) {      //已经加密,进行解密
044                 String encryptedString = matcher.group(1);    //获得加密值
045                 String decryptedPropValue = AesUtils.decrypt(propertyName + SEC_KEY, encryptedString);  //调用AES进行解密,SEC_KEY与属性名联合做密钥更安全
046  
047                 if (decryptedPropValue != null) {  //!=null说明正常
048                     propertyValue = decryptedPropValue; //设置解决后的值
049                 else {//说明解密失败
050                     logger.error("Decrypt " + propertyName + "=" + propertyValue + " error!");
051                 }
052             }
053         }
054  
055         return super.convertProperty(propertyName, propertyValue);  //将处理过的值传给父类继续处理
056     }
057  
058     @Override
059     public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
060         super.postProcessBeanFactory(beanFactory);    //正常执行属性文件加载
061  
062         for (Resource location : locations) {    //加载完后,遍历location,对properties进行加密
063  
064             try {
065                 final File file = location.getFile();
066                 if (file.isFile()) {  //如果是一个普通文件
067  
068                     if (file.canWrite()) { //如果有写权限
069                         encrypt(file);   //调用文件加密方法
070                     else {
071                         if (logger.isWarnEnabled()) {
072                             logger.warn("File '" + location + "' can not be write!");
073                         }
074                     }
075  
076                 else {
077                     if (logger.isWarnEnabled()) {
078                         logger.warn("File '" + location + "' is not a normal file!");
079                     }
080                 }
081  
082             catch (IOException e) {
083                 if (logger.isWarnEnabled()) {
084                     logger.warn("File '" + location + "' is not a normal file!");
085                 }
086             }
087         }
088  
089     }
090  
091     private boolean isBlank(String str) {
092         int strLen;
093         if (str == null || (strLen = str.length()) == 0) {
094             return true;
095         }
096         for (int i = 0; i < strLen; i++) {
097             if ((Character.isWhitespace(str.charAt(i)) == false)) {
098                 return false;
099             }
100         }
101         return true;
102     }
103  
104     private boolean isNotBlank(String str) {
105         return !isBlank(str);
106     }
107  
108  
109     /**
110      * 属性文件加密方法
111      *
112      * @param file
113      */
114     private void encrypt(File file) {
115  
116         List<String> outputLine = new ArrayList<String>();   //定义输出行缓存
117  
118         boolean doEncrypt = false;     //是否加密属性文件标识
119  
120  
121         BufferedReader bufferedReader = null;
122         try {
123  
124             bufferedReader = new BufferedReader(new FileReader(file));
125  
126             String line = null;
127             do {
128  
129                 line = bufferedReader.readLine(); //按行读取属性文件
130                 if (line != null) { //判断是否文件结束
131                     if (isNotBlank(line)) {   //是否为空行
132                         line = line.trim();    //取掉左右空格
133                         if (!line.startsWith("#")) {//如果是非注释行
134                             String[] lineParts = line.split("=");  //将属性名与值分离
135                             String key = lineParts[0];       // 属性名
136                             String value = lineParts[1];      //属性值
137                             if (key != null && value != null) {
138                                 if (encryptedProps.contains(key)) { //发现是加密属性
139                                     final Matcher matcher = encryptedPattern.matcher(value);
140                                     if (!matcher.matches()) { //如果是非加密格式,则`进行加密
141  
142                                         value = ENCRYPTED_PREFIX + AesUtils.encrypt(key + SEC_KEY, value) + ENCRYPTED_SUFFIX;   //进行加密,SEC_KEY与属性名联合做密钥更安全
143  
144                                         line = key + "=" + value;  //生成新一行的加密串
145  
146                                         doEncrypt = true;    //设置加密属性文件标识
147  
148                                         if (logger.isDebugEnabled()) {
149                                             logger.debug("encrypt property:" + key);
150                                         }
151                                     }
152                                 }
153                             }
154                         }
155                     }
156                     outputLine.add(line);
157                 }
158  
159             while (line != null);
160  
161  
162         catch (FileNotFoundException e) {
163             logger.error(e.getMessage(), e);
164         catch (IOException e) {
165             logger.error(e.getMessage(), e);
166         finally {
167             if (bufferedReader != null) {
168                 try {
169                     bufferedReader.close();
170                 catch (IOException e) {
171                     logger.error(e.getMessage(), e);
172                 }
173             }
174         }
175  
176         if (doEncrypt) {      //判断属性文件加密标识
177             BufferedWriter bufferedWriter = null;
178             File tmpFile = null;
179             try {
180                 tmpFile = File.createTempFile(file.getName(), null, file.getParentFile());   //创建临时文件
181  
182                 if (logger.isDebugEnabled()) {
183                     logger.debug("Create tmp file '" + tmpFile.getAbsolutePath() + "'.");
184                 }
185  
186                 bufferedWriter = new BufferedWriter(new FileWriter(tmpFile));
187  
188                 final Iterator<String> iterator = outputLine.iterator();
189                 while (iterator.hasNext()) {                           //将加密后内容写入临时文件
190                     bufferedWriter.write(iterator.next());
191                     if (iterator.hasNext()) {
192                         bufferedWriter.newLine();
193                     }
194                 }
195  
196                 bufferedWriter.flush();
197             catch (IOException e) {
198                 logger.error(e.getMessage(), e);
199             finally {
200                 if (bufferedWriter != null) {
201                     try {
202                         bufferedWriter.close();
203                     catch (IOException e) {
204                         logger.error(e.getMessage(), e);
205                     }
206                 }
207             }
208  
209             File backupFile = new File(file.getAbsoluteFile() + "_" + System.currentTimeMillis());  //准备备份文件名
210  
211             //以下为备份,异常恢复机制
212             if (!file.renameTo(backupFile)) {   //重命名原properties文件,(备份)
213                 logger.error("Could not encrypt the file '" + file.getAbsoluteFile() + "'! Backup the file failed!");
214                 tmpFile.delete(); //删除临时文件
215             else {
216                 if (logger.isDebugEnabled()) {
217                     logger.debug("Backup the file '" + backupFile.getAbsolutePath() + "'.");
218                 }
219  
220                 if (!tmpFile.renameTo(file)) {   //临时文件重命名失败 (加密文件替换原失败)
221                     logger.error("Could not encrypt the file '" + file.getAbsoluteFile() + "'! Rename the tmp file failed!");
222  
223                     if (backupFile.renameTo(file)) {   //恢复备份
224                         if (logger.isInfoEnabled()) {
225                             logger.info("Restore the backup, success.");
226                         }
227                     else {
228                         logger.error("Restore the backup, failed!");
229                     }
230                 else {  //(加密文件替换原成功)
231  
232                     if (logger.isDebugEnabled()) {
233                         logger.debug("Rename the file '" + tmpFile.getAbsolutePath() + "' -> '" + file.getAbsoluteFile() + "'.");
234                     }
235  
236                     boolean dBackup = backupFile.delete();//删除备份文件
237  
238                     if (logger.isDebugEnabled()) {
239                         logger.debug("Delete the backup '" + backupFile.getAbsolutePath() + "'.(" + dBackup + ")");
240                     }
241                 }
242             }
243  
244  
245         }
246  
247     }
248  
249     protected Resource[] locations;
250  
251     @Override
252     public void setLocations(Resource[] locations) {   //由于location是父类私有,所以需要记录到本类的locations中
253         super.setLocations(locations);
254         this.locations = locations;
255     }
256  
257     @Override
258     public void setLocation(Resource location) {   //由于location是父类私有,所以需要记录到本类的locations中
259         super.setLocation(location);
260         this.locations = new Resource[]{location};
261     }
262 }

3、spring.xml

01 <?xml version="1.0" encoding="UTF-8"?>
02 <beans xmlns="http://www.springframework.org/schema/beans"
03        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
04        xmlns:context="http://www.springframework.org/schema/context"
05        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
06  
07     <context:property-placeholder location="/WEB-INF/spring/spring.properties"/>
08  
09     <!--对spring.properties配置文件中的指定属性进行加密-->
10     <bean id="encryptPropertyPlaceholderConfigurer"
11           class="org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer">
12         <property name="locations">
13             <list>
14                 <value>/WEB-INF/spring/spring.properties</value>
15             </list>
16         </property>
17         <property name="encryptedProps">
18             <set>
19                 <value>db.jdbc.username</value>
20                 <value>db.jdbc.password</value>
21                 <value>db.jdbc.url</value>
22             </set>
23         </property>
24     </bean>
25  
26 </beans>

四、运行效果

1、日志

1 [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.url
2 [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.username
3 [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.password
4 [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - Create tmp file '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties2420183175827237221.tmp'.
5 [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - Backup the file '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties_1379959755837'.
6 [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - Rename the file '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties2420183175827237221.tmp' -> '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties'.
7 [RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - Delete the backup '/nautilus/workspaces/idea/spring-prop-encrypt/target/spring-prop-encrypt-1.0-SNAPSHOT/WEB-INF/spring/spring.properties_1379959755837'.(true)

2、原属性文件

1 db.jdbc.driver=com.mysql.jdbc.Driver
2 db.jdbc.url=jdbc:mysql://localhost:3306/noah?useUnicode=true&characterEncoding=utf8
3 db.jdbc.username=noah
4 db.jdbc.password=noah

3、加密后的文件

1 db.jdbc.driver=com.mysql.jdbc.Driver
2 db.jdbc.url=Encrypted:{e5ShuhQjzDZrkqoVdaO6XNQrTqCPIWv8i_VR4zaK28BrmWS_ocagv3weYNdr0WwI}
3 db.jdbc.username=Encrypted:{z5aneQi_h4mk4LEqhjZU-A}
4 db.jdbc.password=Encrypted:{v09a0SrOGbw-_DxZKieu5w}
注:因为密钥与属性名有关,所以相同值加密后的内容也不同,而且不能互换值。

 

五、源码下载

附件地址:http://sdrv.ms/18li77V

六、总结

在成熟加密框架中jasypt(http://www.jasypt.org/)很不错,包含了spring,hibernate等等加密。试用了一些功能后感觉并不太适合我的需要。

加密的安全性是相对的,没有绝对安全的东西。如果有人反编译了加密程序获得了加密解密算法也属正常。希望大家不要因为是否绝对安全而讨论不休。

如果追求更高级别的加密可以考虑混淆class的同时对class文件本身进行加密,改写默认的classloader加载加密class(调用本地核心加密程序,非Java)。

 

 

分享到:
评论
1 楼 sdyyccn 2015-10-10  
很详细,很赞

相关推荐

    在Spring中使用加密外部属性文件

    Spring默认的`PropertyPlaceholderConfigurer`不直接支持加密的属性文件,但它提供了扩展点,允许我们自定义实现来处理加密后的属性。以下是一种实现方式: 1. 创建一个自定义的`PropertyPlaceholderConfigurer`...

    Spring属性占位符PropertyPlaceholderConfigurer的使用

    在Spring框架中,属性占位符`PropertyPlaceholderConfigurer`是一个重要的工具,用于处理配置文件中的属性值引用。它使得我们可以在XML配置文件中使用占位符`${...}`来引用外部属性文件中的值,从而使应用配置更加...

    Spring中PropertyPlaceholderConfigurer的使用

    PropertyPlaceholderConfigurer 不仅可以用于加载 Properties 文件,还可以用于实现其他功能,例如属性文件加密解密等。在后续的文章中,我们将继续讨论这些扩展应用。 PropertyPlaceholderConfigurer 是 Spring ...

    Spring PropertyPlaceholderConfigurer配置文件加载器集成ZooKeeper来实现远程配置读取

    在Spring中,`PropertyPlaceholderConfigurer`是一个非常重要的类,它用于处理属性文件中的占位符,将它们替换为实际的值。这在配置管理中起到了关键作用,特别是在大型分布式系统中,动态配置管理变得尤为重要。...

    Spring如何使用PropertyPlaceholderConfigurer读取文件

    在上面的配置中,我们可以通过location属性指定一个配置文件的路径,也可以通过locations属性指定多个配置文件的路径。PropertyPlaceholderConfigurer会自动读取这些配置文件,并将其转换为Properties对象。 此外,...

    spring,配置文件从属性文件读取JDBC连接的相关参数

    在本例中,`jdbc.properties`是一个属性文件,它包含了JDBC连接所需的全部或部分参数。以下是如何在Spring中从这个文件中读取这些参数的步骤: 1. **创建属性文件**: 首先,我们需要创建一个名为`jdbc.properties...

    Spring数据库连接等配置加密

    `propertyConfigurer`则告诉Spring使用解密器来处理配置文件中的加密属性。 最后,关于`junfeng`这个文件,它可能是作者在文章中提到的一种特定的加密方式或者一个工具的名称。由于没有具体的文件内容,这里无法给...

    spring使用属性文件

    `PropertyPlaceholderConfigurer`是一个Bean定义后处理器,它会替换Bean定义中的占位符(如`${key}`)为属性文件中的值。而`@PropertySource`注解可以直接在类级别上声明,指示Spring从指定的属性文件中读取属性。 ...

    bboss ioc配置文件中使用外部属性文件介绍

    本文将详细介绍如何在BBoss的IOC配置文件中引用外部属性文件,以便更好地管理和动态配置应用。 首先,BBoss的IOC配置文件通常是一个XML文件,比如`bboss-ioc.xml`,在这个文件中我们可以声明并配置各种bean。当需要...

    Spring配置加密方案收集.pdf

    本文将详细介绍一个基于Spring 2.5的配置文件加密方案,该方案涉及对`.properties`属性文件中的变量值进行加密,并在Spring启动时自动解密。 首先,我们需要创建一个属性文件,例如`mail.properties`,包含需要加密...

    spring配置文件加密方法示例

    本文将详细介绍如何在Spring中对配置文件进行加密,以保护这些敏感数据。 首先,我们需要理解Spring配置文件的基本结构。一个典型的Spring配置文件(如`applicationContext.xml`)是一个XML文档,它包含Bean的定义...

    org.springframework.beans.factory.config.PropertyPlaceholderConfigurer

    这里,`PropertyPlaceholderConfigurer` 会查找指定位置(例如 `config.properties`)的属性文件,并将其中的键值对与XML或Java配置中的占位符进行匹配替换。这使得我们的配置更加灵活,可以将一些敏感信息(如...

    属性占位符配置器

    该类作为Spring的容器后处理器,在应用程序上下文初始化阶段自动读取指定的属性文件,并将其中的键值对映射到配置文件中的占位符上,从而完成具体的配置替换。这种机制特别适用于以下场景: - **数据库配置**:如...

    spring 启动时加载不同的文件

    - **功能**: 它能够将`properties`文件中的键值对映射到Spring容器中,供其他Bean使用。 **2. 动态加载配置文件** - **目的**: 根据部署环境的不同,动态地更改应用使用的配置文件,从而实现环境间的灵活切换。 - *...

    SSM 读取properties文件

    此外,Spring Boot引入了更强大的`@ConfigurationProperties`,它能自动绑定properties文件中的键值对到bean的属性上,提供了更面向对象的配置方式。但这个特性属于Spring Boot范畴,不属于SSM的经典组合。 综上所...

    说说在Spring中如何引用外部属性文件的方法

    在上面的配置中,我们使用 PropertyPlaceholderConfigurer 来引入外部属性文件 system.properties,然后在数据源 Bean 中使用占位符 ${} 来引用属性文件中的属性项。 PropertyPlaceholderConfigurer 属性说明 ...

    4.Spring应用扩展.pptx

    例如,可以将数据库连接信息写入属性文件,如`database.properties`,然后在Spring配置文件中使用`PropertyPlaceholderConfigurer`引入属性文件。这样,我们就可以在XML配置中使用`${...}`的方式来引用属性值,如`${...

    Spring3.0 配置文件中加载Properties文件的小例子

    接下来,我们将在Spring的配置文件(如`applicationContext.xml`)中声明一个`PropertyPlaceholderConfigurer` bean,它负责加载并解析Properties文件。配置如下: ```xml class="org.springframework.beans....

    Spring项目application.xml配置文件加解密

    `EncryptPropertyPlaceholderConfigurer.java`就是这样一个类,它扩展了Spring的`PropertyPlaceholderConfigurer`,并覆盖了`resolvePlaceholder`方法来处理加密的属性值。 ```java import org.springframework....

Global site tag (gtag.js) - Google Analytics