處於安全考慮須要對.properties中的數據庫用戶名與密碼等敏感數據進行加密。項目中使用了Spring3框架統一加載屬性文件,因此最好能夠干擾這個加載過程來實現對.properties文件中的部分屬性進行加密。 java
屬性文件中的屬性最初始時敏感屬性值能夠爲明文,程序第一次執行後自動加密明文爲密文。 mysql
修正了一個小bug,當屬性值中包含「=」號時會被截斷。但仍是沒有徹底按Java Properties標準進行實現(沒考慮「:」、"\"等狀況)。 算法
注:aes包中爲AES加密工具類,能夠根據加密習慣自行修改 spring
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}; } }
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]; //屬性值 int eIndex = line.indexOf("="); //將屬性名與值分離(修正1) String key = line.substring(0,eIndex); // 屬性名 String value = line.substring(eIndex+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}; } }注意:134行的變化,把split換爲了indexof
<?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>
[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)
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
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}注:由於密鑰與屬性名有關,因此相同值加密後的內容也不一樣,並且不能互換值。
附件地址:http://sdrv.ms/1eguILi sql
在成熟加密框架中jasypt(http://www.jasypt.org/)很不錯,包含了spring,hibernate等等加密。試用了一些功能後感受並不太適合個人須要。 shell
加密的安全性是相對的,沒有絕對安全的東西。若是有人反編譯了加密程序得到了加密解密算法也屬正常。但願你們不要由於是否絕對安全而討論不休。 數據庫
若是追求更高級別的加密能夠考慮混淆class的同時對class文件自己進行加密,改寫默認的classloader加載加密class(調用本地核心加密程序,非Java)。 緩存