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

jopen 11年前

一、背景

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

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

二、问题分析

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

三、程序开发

1、目录结构

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

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

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

3、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>

四、运行效果

1、日志

[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、原属性文件

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、加密后的文件

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/18li77V

六、总结

在成熟加密框架中jasypt(http://www.jasypt.org/)很不错,包含了spring,hibernate等等加密。试用了一些功能后感觉并不太适合我的需要。

加密的安全性是相对的,没有绝对安全的东西。如果有人反编译了加密程序获得了加密解密算法也属正常。希望大家不要因为是否绝对安全而讨论不休。

如果追求更高级别的加密可以考虑混淆class的同时对class文件本身进行加密,改写默认的classloader加载加密class(调用本地核心加密程序,非Java)。