擴展PropertyPlaceholderConfigurer對prop文件中的屬性加密(修正1)

1、背景

處於安全考慮須要對.properties中的數據庫用戶名與密碼等敏感數據進行加密。項目中使用了Spring3框架統一加載屬性文件,因此最好能夠干擾這個加載過程來實現對.properties文件中的部分屬性進行加密。 java

屬性文件中的屬性最初始時敏感屬性值能夠爲明文,程序第一次執行後自動加密明文爲密文。 mysql

修正1:

修正了一個小bug,當屬性值中包含「=」號時會被截斷。但仍是沒有徹底按Java Properties標準進行實現(沒考慮「:」、"\"等狀況)。 算法

2、問題分析

  1. 擴展PropertyPlaceholderConfigurer最好的方式就是編寫一個繼承該類的子類。
  2. 外部設置locations時,記錄所有locations信息,爲加密文件保留屬性文件列表。重寫setLocations與setLocation方法(在父類中locations私有)
  3. 尋找一個讀取屬性文件屬性的環節,檢測敏感屬性加密狀況。對有已有加密特徵的敏感屬性進行解密。重寫convertProperty方法來實現。
  4. 屬性文件第一次加載完畢後,當即對屬性文件中的明文信息進行加密。重寫postProcessBeanFactory方式來實現。

3、程序開發

一、目錄結構

注:aes包中爲AES加密工具類,能夠根據加密習慣自行修改 spring

二、EncryptPropertyPlaceholderConfigurer(詳見註釋)

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};
    }
}

二、EncryptPropertyPlaceholderConfigurer(詳見註釋)(修正1)

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

三、spring.xml

<?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>

4、運行效果

一、日誌

[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}
注:由於密鑰與屬性名有關,因此相同值加密後的內容也不一樣,並且不能互換值。


5、源碼下載

附件地址:http://sdrv.ms/1eguILi sql

6、總結

在成熟加密框架中jasypt(http://www.jasypt.org/)很不錯,包含了spring,hibernate等等加密。試用了一些功能後感受並不太適合個人須要。 shell

加密的安全性是相對的,沒有絕對安全的東西。若是有人反編譯了加密程序得到了加密解密算法也屬正常。但願你們不要由於是否絕對安全而討論不休。 數據庫

若是追求更高級別的加密能夠考慮混淆class的同時對class文件自己進行加密,改寫默認的classloader加載加密class(調用本地核心加密程序,非Java)。 緩存

相關文章
相關標籤/搜索