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

一、背景

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

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

二、问题分析

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

三、程序开发

1、目录结构

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

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

2、EncryptPropertyPlaceholderConfigurer(详见注释)
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
package org.noahx.spring.propencrypt;
 
import org.noahx.spring.propencrypt.aes.AesUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.core.io.Resource;
 
import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
 
/**
  * Created with IntelliJ IDEA.
  * User: noah
  * Date: 9/16/13
  * Time: 10:36 AM
  * To change this template use File | Settings | File Templates.
  */
public class EncryptPropertyPlaceholderConfigurer  extends PropertyPlaceholderConfigurer {
 
     private static final String SEC_KEY =  "@^_^123aBcZ*" ;     //主密钥
     private static final String ENCRYPTED_PREFIX =  "Encrypted:{" ;
     private static final String ENCRYPTED_SUFFIX =  "}" ;
     private static Pattern encryptedPattern = Pattern.compile( "Encrypted:\\{((\\w|\\-)*)\\}" );   //加密属性特征正则
 
     private Logger logger = LoggerFactory.getLogger( this .getClass());
 
     private Set<String> encryptedProps = Collections.emptySet();
 
     public void setEncryptedProps(Set<String> encryptedProps) {
         this .encryptedProps = encryptedProps;
     }
 
     @Override
     protected String convertProperty(String propertyName, String propertyValue) {
 
         if (encryptedProps.contains(propertyName)) {  //如果在加密属性名单中发现该属性
             final Matcher matcher = encryptedPattern.matcher(propertyValue);   //判断该属性是否已经加密
             if (matcher.matches()) {       //已经加密,进行解密
                 String encryptedString = matcher.group( 1 );     //获得加密值
                 String decryptedPropValue = AesUtils.decrypt(propertyName + SEC_KEY, encryptedString);   //调用AES进行解密,SEC_KEY与属性名联合做密钥更安全
 
                 if (decryptedPropValue !=  null ) {   //!=null说明正常
                     propertyValue = decryptedPropValue;  //设置解决后的值
                 else { //说明解密失败
                     logger.error( "Decrypt " + propertyName +  "=" + propertyValue +  " error!" );
                 }
             }
         }
 
         return super .convertProperty(propertyName, propertyValue);   //将处理过的值传给父类继续处理
     }
 
     @Override
     public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)  throws BeansException {
         super .postProcessBeanFactory(beanFactory);     //正常执行属性文件加载
 
         for (Resource location : locations) {     //加载完后,遍历location,对properties进行加密
 
             try {
                 final File file = location.getFile();
                 if (file.isFile()) {   //如果是一个普通文件
 
                     if (file.canWrite()) {  //如果有写权限
                         encrypt(file);    //调用文件加密方法
                     else {
                         if (logger.isWarnEnabled()) {
                             logger.warn( "File '" + location +  "' can not be write!" );
                         }
                     }
 
                 else {
                     if (logger.isWarnEnabled()) {
                         logger.warn( "File '" + location +  "' is not a normal file!" );
                     }
                 }
 
             catch (IOException e) {
                 if (logger.isWarnEnabled()) {
                     logger.warn( "File '" + location +  "' is not a normal file!" );
                 }
             }
         }
 
     }
 
     private boolean isBlank(String str) {
         int strLen;
         if (str ==  null || (strLen = str.length()) ==  0 ) {
             return true ;
         }
         for ( int i =  0 ; i < strLen; i++) {
             if ((Character.isWhitespace(str.charAt(i)) ==  false )) {
                 return false ;
             }
         }
         return true ;
     }
 
     private boolean isNotBlank(String str) {
         return !isBlank(str);
     }
 
 
     /**
      * 属性文件加密方法
      *
      * @param file
      */
     private void encrypt(File file) {
 
         List<String> outputLine =  new ArrayList<String>();    //定义输出行缓存
 
         boolean doEncrypt =  false ;      //是否加密属性文件标识
 
 
         BufferedReader bufferedReader =  null ;
         try {
 
             bufferedReader =  new BufferedReader( new FileReader(file));
 
             String line =  null ;
             do {
 
                 line = bufferedReader.readLine();  //按行读取属性文件
                 if (line !=  null ) {  //判断是否文件结束
                     if (isNotBlank(line)) {    //是否为空行
                         line = line.trim();     //取掉左右空格
                         if (!line.startsWith( "#" )) { //如果是非注释行
                             String[] lineParts = line.split( "=" );   //将属性名与值分离
                             String key = lineParts[ 0 ];        // 属性名
                             String value = lineParts[ 1 ];       //属性值
                             if (key !=  null && value !=  null ) {
                                 if (encryptedProps.contains(key)) {  //发现是加密属性
                                     final Matcher matcher = encryptedPattern.matcher(value);
                                     if (!matcher.matches()) {  //如果是非加密格式,则`进行加密
 
                                         value = ENCRYPTED_PREFIX + AesUtils.encrypt(key + SEC_KEY, value) + ENCRYPTED_SUFFIX;    //进行加密,SEC_KEY与属性名联合做密钥更安全
 
                                         line = key +  "=" + value;   //生成新一行的加密串
 
                                         doEncrypt =  true ;     //设置加密属性文件标识
 
                                         if (logger.isDebugEnabled()) {
                                             logger.debug( "encrypt property:" + key);
                                         }
                                     }
                                 }
                             }
                         }
                     }
                     outputLine.add(line);
                 }
 
             while (line !=  null );
 
 
         catch (FileNotFoundException e) {
             logger.error(e.getMessage(), e);
         catch (IOException e) {
             logger.error(e.getMessage(), e);
         finally {
             if (bufferedReader !=  null ) {
                 try {
                     bufferedReader.close();
                 catch (IOException e) {
                     logger.error(e.getMessage(), e);
                 }
             }
         }
 
         if (doEncrypt) {       //判断属性文件加密标识
             BufferedWriter bufferedWriter =  null ;
             File tmpFile =  null ;
             try {
                 tmpFile = File.createTempFile(file.getName(),  null , file.getParentFile());    //创建临时文件
 
                 if (logger.isDebugEnabled()) {
                     logger.debug( "Create tmp file '" + tmpFile.getAbsolutePath() +  "'." );
                 }
 
                 bufferedWriter =  new BufferedWriter( new FileWriter(tmpFile));
 
                 final Iterator<String> iterator = outputLine.iterator();
                 while (iterator.hasNext()) {                            //将加密后内容写入临时文件
                     bufferedWriter.write(iterator.next());
                     if (iterator.hasNext()) {
                         bufferedWriter.newLine();
                     }
                 }
 
                 bufferedWriter.flush();
             catch (IOException e) {
                 logger.error(e.getMessage(), e);
             finally {
                 if (bufferedWriter !=  null ) {
                     try {
                         bufferedWriter.close();
                     catch (IOException e) {
                         logger.error(e.getMessage(), e);
                     }
                 }
             }
 
             File backupFile =  new File(file.getAbsoluteFile() +  "_" + System.currentTimeMillis());   //准备备份文件名
 
             //以下为备份,异常恢复机制
             if (!file.renameTo(backupFile)) {    //重命名原properties文件,(备份)
                 logger.error( "Could not encrypt the file '" + file.getAbsoluteFile() +  "'! Backup the file failed!" );
                 tmpFile.delete();  //删除临时文件
             else {
                 if (logger.isDebugEnabled()) {
                     logger.debug( "Backup the file '" + backupFile.getAbsolutePath() +  "'." );
                 }
 
                 if (!tmpFile.renameTo(file)) {    //临时文件重命名失败 (加密文件替换原失败)
                     logger.error( "Could not encrypt the file '" + file.getAbsoluteFile() +  "'! Rename the tmp file failed!" );
 
                     if (backupFile.renameTo(file)) {    //恢复备份
                         if (logger.isInfoEnabled()) {
                             logger.info( "Restore the backup, success." );
                         }
                     else {
                         logger.error( "Restore the backup, failed!" );
                     }
                 else {   //(加密文件替换原成功)
 
                     if (logger.isDebugEnabled()) {
                         logger.debug( "Rename the file '" + tmpFile.getAbsolutePath() +  "' -> '" + file.getAbsoluteFile() +  "'." );
                     }
 
                     boolean dBackup = backupFile.delete(); //删除备份文件
 
                     if (logger.isDebugEnabled()) {
                         logger.debug( "Delete the backup '" + backupFile.getAbsolutePath() +  "'.(" + dBackup +  ")" );
                     }
                 }
             }
 
 
         }
 
     }
 
     protected Resource[] locations;
 
     @Override
     public void setLocations(Resource[] locations) {    //由于location是父类私有,所以需要记录到本类的locations中
         super .setLocations(locations);
         this .locations = locations;
     }
 
     @Override
     public void setLocation(Resource location) {    //由于location是父类私有,所以需要记录到本类的locations中
         super .setLocation(location);
         this .locations =  new Resource[]{location};
     }
}
3、spring.xml
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<? xml version = "1.0" encoding = "UTF-8" ?>
< beans xmlns = "http://www.springframework.org/schema/beans"
        xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context = "http://www.springframework.org/schema/context"
        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" >
 
     < context:property-placeholder location = "/WEB-INF/spring/spring.properties" />
 
     <!--对spring.properties配置文件中的指定属性进行加密-->
     < bean id = "encryptPropertyPlaceholderConfigurer"
           class = "org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer" >
         < property name = "locations" >
             < list >
                 < value >/WEB-INF/spring/spring.properties</ value >
             </ list >
         </ property >
         < property name = "encryptedProps" >
             < set >
                 < value >db.jdbc.username</ value >
                 < value >db.jdbc.password</ value >
                 < value >db.jdbc.url</ value >
             </ set >
         </ property >
     </ bean >
 
</ beans >

四、运行效果

1、日志
?
1
2
3
4
5
6
7
[RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.url
[RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.username
[RMI TCP Connection(2)-127.0.0.1] DEBUG org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer - encrypt property:db.jdbc.password
[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' .
[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' .
[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' .
[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
2
3
4
db.jdbc.driver=com.mysql.jdbc.Driver
db.jdbc.url=jdbc:mysql: //localhost :3306 /noah ?useUnicode= true &characterEncoding=utf8
db.jdbc.username=noah
db.jdbc.password=noah
3、加密后的文件
?
1
2
3
4
db.jdbc.driver=com.mysql.jdbc.Driver
db.jdbc.url=Encrypted:{e5ShuhQjzDZrkqoVdaO6XNQrTqCPIWv8i_VR4zaK28BrmWS_ocagv3weYNdr0WwI}
db.jdbc.username=Encrypted:{z5aneQi_h4mk4LEqhjZU-A}
db.jdbc.password=Encrypted:{v09a0SrOGbw-_DxZKieu5w}
注:因为密钥与属性名有关,所以相同值加密后的内容也不同,而且不能互换值。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值