一、背景
处于安全考虑需要对.properties中的数据库用户名与密码等敏感数据进行加密。项目中使用了Spring3框架统一加载属性文件,所以最好可以干扰这个加载过程来实现对.properties文件中的部分属性进行加密。
属性文件中的属性最初始时敏感属性值可以为明文,程序第一次执行后自动加密明文为密文。
二、问题分析
- 扩展PropertyPlaceholderConfigurer最好的方式就是编写一个继承该类的子类。
- 外部设置locations时,记录全部locations信息,为加密文件保留属性文件列表。重写setLocations与setLocation方法(在父类中locations私有)
- 寻找一个读取属性文件属性的环节,检测敏感属性加密情况。对有已有加密特征的敏感属性进行解密。重写convertProperty方法来实现。
- 属性文件第一次加载完毕后,立即对属性文件中的明文信息进行加密。重写postProcessBeanFactory方式来实现。
三、程序开发
1、目录结构
![](http://static.oschina.net/uploads/space/2013/0924/021250_urfr_569848.png)
注:aes包中为AES加密工具类,可以根据加密习惯自行修改
2、EncryptPropertyPlaceholderConfigurer(详见注释)
001 | package org.noahx.spring.propencrypt; |
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; |
013 | import java.util.regex.Matcher; |
014 | import java.util.regex.Pattern; |
017 | * Created with IntelliJ IDEA. |
021 | * To change this template use File | Settings | File Templates. |
023 | public class EncryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer { |
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|\\-)*)\\}" ); |
030 | private Logger logger = LoggerFactory.getLogger( this .getClass()); |
032 | private Set<String> encryptedProps = Collections.emptySet(); |
034 | public void setEncryptedProps(Set<String> encryptedProps) { |
035 | this .encryptedProps = encryptedProps; |
039 | protected String convertProperty(String propertyName, String propertyValue) { |
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); |
047 | if (decryptedPropValue != null ) { |
048 | propertyValue = decryptedPropValue; |
050 | logger.error( "Decrypt " + propertyName + "=" + propertyValue + " error!" ); |
055 | return super .convertProperty(propertyName, propertyValue); |
059 | public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { |
060 | super .postProcessBeanFactory(beanFactory); |
062 | for (Resource location : locations) { |
065 | final File file = location.getFile(); |
068 | if (file.canWrite()) { |
071 | if (logger.isWarnEnabled()) { |
072 | logger.warn( "File '" + location + "' can not be write!" ); |
077 | if (logger.isWarnEnabled()) { |
078 | logger.warn( "File '" + location + "' is not a normal file!" ); |
082 | } catch (IOException e) { |
083 | if (logger.isWarnEnabled()) { |
084 | logger.warn( "File '" + location + "' is not a normal file!" ); |
091 | private boolean isBlank(String str) { |
093 | if (str == null || (strLen = str.length()) == 0 ) { |
096 | for ( int i = 0 ; i < strLen; i++) { |
097 | if ((Character.isWhitespace(str.charAt(i)) == false )) { |
104 | private boolean isNotBlank(String str) { |
105 | return !isBlank(str); |
114 | private void encrypt(File file) { |
116 | List<String> outputLine = new ArrayList<String>(); |
118 | boolean doEncrypt = false ; |
121 | BufferedReader bufferedReader = null ; |
124 | bufferedReader = new BufferedReader( new FileReader(file)); |
129 | line = bufferedReader.readLine(); |
131 | if (isNotBlank(line)) { |
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()) { |
142 | value = ENCRYPTED_PREFIX + AesUtils.encrypt(key + SEC_KEY, value) + ENCRYPTED_SUFFIX; |
144 | line = key + "=" + value; |
148 | if (logger.isDebugEnabled()) { |
149 | logger.debug( "encrypt property:" + key); |
156 | outputLine.add(line); |
159 | } while (line != null ); |
162 | } catch (FileNotFoundException e) { |
163 | logger.error(e.getMessage(), e); |
164 | } catch (IOException e) { |
165 | logger.error(e.getMessage(), e); |
167 | if (bufferedReader != null ) { |
169 | bufferedReader.close(); |
170 | } catch (IOException e) { |
171 | logger.error(e.getMessage(), e); |
177 | BufferedWriter bufferedWriter = null ; |
180 | tmpFile = File.createTempFile(file.getName(), null , file.getParentFile()); |
182 | if (logger.isDebugEnabled()) { |
183 | logger.debug( "Create tmp file '" + tmpFile.getAbsolutePath() + "'." ); |
186 | bufferedWriter = new BufferedWriter( new FileWriter(tmpFile)); |
188 | final Iterator<String> iterator = outputLine.iterator(); |
189 | while (iterator.hasNext()) { |
190 | bufferedWriter.write(iterator.next()); |
191 | if (iterator.hasNext()) { |
192 | bufferedWriter.newLine(); |
196 | bufferedWriter.flush(); |
197 | } catch (IOException e) { |
198 | logger.error(e.getMessage(), e); |
200 | if (bufferedWriter != null ) { |
202 | bufferedWriter.close(); |
203 | } catch (IOException e) { |
204 | logger.error(e.getMessage(), e); |
209 | File backupFile = new File(file.getAbsoluteFile() + "_" + System.currentTimeMillis()); |
212 | if (!file.renameTo(backupFile)) { |
213 | logger.error( "Could not encrypt the file '" + file.getAbsoluteFile() + "'! Backup the file failed!" ); |
216 | if (logger.isDebugEnabled()) { |
217 | logger.debug( "Backup the file '" + backupFile.getAbsolutePath() + "'." ); |
220 | if (!tmpFile.renameTo(file)) { |
221 | logger.error( "Could not encrypt the file '" + file.getAbsoluteFile() + "'! Rename the tmp file failed!" ); |
223 | if (backupFile.renameTo(file)) { |
224 | if (logger.isInfoEnabled()) { |
225 | logger.info( "Restore the backup, success." ); |
228 | logger.error( "Restore the backup, failed!" ); |
232 | if (logger.isDebugEnabled()) { |
233 | logger.debug( "Rename the file '" + tmpFile.getAbsolutePath() + "' -> '" + file.getAbsoluteFile() + "'." ); |
236 | boolean dBackup = backupFile.delete(); |
238 | if (logger.isDebugEnabled()) { |
239 | logger.debug( "Delete the backup '" + backupFile.getAbsolutePath() + "'.(" + dBackup + ")" ); |
249 | protected Resource[] locations; |
252 | public void setLocations(Resource[] locations) { |
253 | super .setLocations(locations); |
254 | this .locations = locations; |
258 | public void setLocation(Resource location) { |
259 | super .setLocation(location); |
260 | this .locations = new Resource[]{location}; |
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" > |
07 | < context:property-placeholder location = "/WEB-INF/spring/spring.properties" /> |
10 | < bean id = "encryptPropertyPlaceholderConfigurer" |
11 | class = "org.noahx.spring.propencrypt.EncryptPropertyPlaceholderConfigurer" > |
12 | < property name = "locations" > |
14 | < value >/WEB-INF/spring/spring.properties</ value > |
17 | < property name = "encryptedProps" > |
19 | < value >db.jdbc.username</ value > |
20 | < value >db.jdbc.password</ value > |
21 | < value >db.jdbc.url</ value > |
四、运行效果
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、加密后的文件
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)。