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

来源:互联网 发布:mac质地分类 编辑:程序博客网 时间:2024/06/04 17:56

一、背景

处于安全考虑需要对.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
packageorg.noahx.spring.propencrypt;
 
importorg.noahx.spring.propencrypt.aes.AesUtils;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.beans.BeansException;
importorg.springframework.beans.factory.config.ConfigurableListableBeanFactory;
importorg.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
importorg.springframework.core.io.Resource;
 
importjava.io.*;
importjava.util.*;
importjava.util.regex.Matcher;
importjava.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.
 */
publicclass EncryptPropertyPlaceholderConfigurer extendsPropertyPlaceholderConfigurer {
 
    privatestatic final String SEC_KEY = "@^_^123aBcZ*";    //主密钥
    privatestatic final String ENCRYPTED_PREFIX = "Encrypted:{";
    privatestatic final String ENCRYPTED_SUFFIX = "}";
    privatestatic Pattern encryptedPattern = Pattern.compile("Encrypted:\\{((\\w|\\-)*)\\}");  //加密属性特征正则
 
    privateLogger logger = LoggerFactory.getLogger(this.getClass());
 
    privateSet<String> encryptedProps = Collections.emptySet();
 
    publicvoid setEncryptedProps(Set<String> encryptedProps) {
        this.encryptedProps = encryptedProps;
    }
 
    @Override
    protectedString convertProperty(String propertyName, String propertyValue) {
 
        if(encryptedProps.contains(propertyName)) { //如果在加密属性名单中发现该属性
            finalMatcher 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!");
                }
            }
        }
 
        returnsuper.convertProperty(propertyName, propertyValue);  //将处理过的值传给父类继续处理
    }
 
    @Override
    publicvoid postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throwsBeansException {
        super.postProcessBeanFactory(beanFactory);    //正常执行属性文件加载
 
        for(Resource location : locations) {    //加载完后,遍历location,对properties进行加密
 
            try{
                finalFile 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!");
                }
            }
        }
 
    }
 
    privateboolean isBlank(String str) {
        intstrLen;
        if(str == null|| (strLen = str.length()) == 0) {
            returntrue;
        }
        for(inti = 0; i < strLen; i++) {
            if((Character.isWhitespace(str.charAt(i)) == false)) {
                returnfalse;
            }
        }
        returntrue;
    }
 
    privateboolean isNotBlank(String str) {
        return!isBlank(str);
    }
 
 
    /**
     * 属性文件加密方法
     *
     * @param file
     */
    privatevoid encrypt(File file) {
 
        List<String> outputLine = newArrayList<String>();   //定义输出行缓存
 
        booleandoEncrypt = false;     //是否加密属性文件标识
 
 
        BufferedReader bufferedReader = null;
        try{
 
            bufferedReader = newBufferedReader(newFileReader(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)) { //发现是加密属性
                                    finalMatcher 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 = newBufferedWriter(newFileWriter(tmpFile));
 
                finalIterator<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 = newFile(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() + "'.");
                    }
 
                    booleandBackup = backupFile.delete();//删除备份文件
 
                    if(logger.isDebugEnabled()) {
                        logger.debug("Delete the backup '" + backupFile.getAbsolutePath() + "'.("+ dBackup + ")");
                    }
                }
            }
 
 
        }
 
    }
 
    protectedResource[] locations;
 
    @Override
    publicvoid setLocations(Resource[] locations) {   //由于location是父类私有,所以需要记录到本类的locations中
        super.setLocations(locations);
        this.locations = locations;
    }
 
    @Override
    publicvoid setLocation(Resource location) {   //由于location是父类私有,所以需要记录到本类的locations中
        super.setLocation(location);
        this.locations = newResource[]{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
<?xmlversion="1.0"encoding="UTF-8"?>
<beansxmlns="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-placeholderlocation="/WEB-INF/spring/spring.properties"/>
 
    <!--对spring.properties配置文件中的指定属性进行加密-->
    <beanid="encryptPropertyPlaceholderConfigurer"
          class="org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer">
        <propertyname="locations">
            <list>
                <value>/WEB-INF/spring/spring.properties</value>
            </list>
        </property>
        <propertyname="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 0
原创粉丝点击