`

在Spring中如何使用加密外部属性文件

阅读更多
   在Spring的开发中,我们在很多情况下会使用占位符引用属性文件的属性值来简化我们的系统及使我们的系统具有更高的灵活性和通用性。这种配置方式有两个明显的好处:
?- 减少维护的工作量:资源的配置信息可以多应用共享,在多个应用使用同一资源的情况下,如果资源的地址、用户名等配置信息发生了更改,你只要调整属性文件就可以了;
?- 使部署更简单:Spring配置文件主要描述应用程序中的Bean,这些配置信息在开发完成后,应该就固定下来了,在部署应用时,需要根据部署环境调整是就是数据源,邮件服务器的配置信息,将它们的配置信息独立到属性文件中,应用部署人员只需要调整资源属性文件即可,根本不需要关注内容复杂的Spring 配置文件。不仅给部署和维护带来了方便,也降低了出错的机率。
    Spring为我们提供了一个BeanFactoryPostProcessorBean工厂后置处理器接口的实现类:org.springframework.beans.factory.config.PropertyPlaceholderConfigurer,它的主要功能是对引用了外部属性值的<bean>进行处理,将其翻译成真实的配置值。
    一般的属性信息以明文的方式存放在属性文件中并没有什么问题,但如果是数据源或邮件服务器用户名密码等重要的信息,在某些场合,我们可能需要以密文的方式保存。虽然Web应用的客户端用户看不到配置文件的,但有时,我们只希望特定的维护人员掌握重要资源的配置信息,而不是毫无保留地对所有可以进入部署机器的用户开放。
    对于这种具有高度安全性要求的系统(如电信、银行、重点人口库等),我们需要对资源连接等属性配置文件中的配置信息加密存放。然后让Spring容器启动时,读入配置文件后,先进行解密,然后再进行占位符的替换。
    很可惜,PropertyPlaceholderConfigurer只支持明文的属性文件。但是,我们可以充分利用Spring框架的扩展性,通过扩展 PropertyPlaceholderConfigurer类来达到我们的要求。本文将讲解使用加密属性文件的原理并提供具体的实现。

    以传统的方式使用属性文件
    一般情况下,外部属性文件用于定义诸如数据源或邮件服务器之类的配置信息。这里,我们通过一个简单的例子,讲解使用属性文件的方法。假设有一个car.properties属性文件,文件内容如下:
   
brand=红旗CA72 
    maxSpeed=250 
    price=20000.00

    该文件放在类路径的com/baobaotao/目录下,在Spring配置文件中利用PropertyPlaceholderConfigurer引入这个配置文件,并通过占位符引用属性文件内的属性项,如代码清单 1所示:
代码清单 1 使用外部属性文件进行配置
Java代码
<!-- ① 引入外部属性文件 -->  
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">  
    <property name="locations">  
       <list>  
          <value>classpath:com/baobaotao/car.properties</value> ② 指定属性文件地址  
       </list>  
    </property>  
    <property name="fileEncoding" value="utf-8"/>  
</bean>  
<!-- ③ 引用外部属性的值,对car进行配置 -->  
<bean id="car" class="com.baobaotao.place.Car">  
    <property name="brand" value="${brand}" />  
    <property name="maxSpeed" value="${maxSpeed}" />  
    <property name="price" value="${price}" />  
</bean>  
<!-- ① 引入外部属性文件 -->
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
    <property name="locations">
       <list>
          <value>classpath:com/baobaotao/car.properties</value> ② 指定属性文件地址
       </list>
    </property>
    <property name="fileEncoding" value="utf-8"/>
</bean>
<!-- ③ 引用外部属性的值,对car进行配置 -->
<bean id="car" class="com.baobaotao.place.Car">
    <property name="brand" value="${brand}" />
    <property name="maxSpeed" value="${maxSpeed}" />
    <property name="price" value="${price}" />
</bean>

    在①处,我们通过PropertyPlaceholderConfigurer这个BeanFactoryPostProcessor实现类引用外部的属性文件,通过它的locations属性指定Spring配置文件中引用到的属性文件,在PropertyPlaceholderConfigurer内部,locations是一个Resource数组,所以你可以在地址前添加资源类型前缀,如②处所示。如果需要引用多个属性文件,只需要在②处添加相应<value>配置项即可。

    分析PropertyPlaceholderConfigurer结构
    我们知道Spring通过PropertyPlaceholderConfigurer提供对外部属性文件的支持,为了使用加密的属性文件,我们就需要分析该类的工作机理,再进行改造。所以我们先来了解一下该类的结构(见附件1)。
    其中PropertiesLoaderSupport类有一个重要的protected void loadProperties(Properties props)方法,查看它的注释,可以知道该方法的作用是将PropertyPlaceholderConfigurer 中locations属性所定义的属性文件的内容读取到props入参对象中。这个方法比较怪,Java很少通过入参承载返回值,但这个方法就是这样。

    所以,我们只要简单地重载这个方法,在将资源文件的内容转换为Properties之前,添加一个解密的步骤就可以了。但是,PropertiesLoaderSupport的设计有一个很让人遗憾的地方,它的locations属性是private的,只提供setter 没有提供getter。因此,无法在子类中获取PropertiesLoaderSupport中的locations(资源地址),所以我们得在子类重新定义locations属性并覆盖PropertiesLoaderSupport中的setLocations()方法。
编写支持加密属性文件的实现类
    通过以上分析,我们设计一个支持加密属性文件的增强型PropertyPlaceholderConfigurer,其代码如所示:
代码清单 2
Java代码
import java.io.IOException;  
import java.io.InputStream;  
import java.io.InputStreamReader;  
import java.security.Key;  
import java.util.Properties;  
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;  
import org.springframework.core.io.Resource;  
import org.springframework.util.DefaultPropertiesPersister;  
import org.springframework.util.PropertiesPersister;  
public class DecryptPropertyPlaceholderConfigurer  
        extends PropertyPlaceholderConfigurer ...{  
    private Resource[] locations;   //① 重新定义父类中的这个同名属性  
    private Resource keyLocation; //② 用于指定密钥文件  
    public void setKeyLocation(Resource keyLocation) ...{  
        this.keyLocation = keyLocation;  
    }  
    public void setLocations(Resource[] locations) ...{  
        this.locations = locations;  
    }  
    public void loadProperties(Properties props) throws IOException ...{  
        if (this.locations != null) ...{  
            PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();  
            for (int i = 0; i < this.locations.length; i++) ...{  
                Resource location = this.locations[i];  
                if (logger.isInfoEnabled()) ...{  
                    logger.info("Loading properties file from " + location);  
                }  
                InputStream is = null;  
                try ...{  
                    is = location.getInputStream();  
                        //③ 加载密钥  
                    Key key = DESEncryptUtil.getKey(keyLocation.getInputStream());  
                    //④ 对属性文件进行解密  
is = DESEncryptUtil.doDecrypt(key, is);  
//⑤ 将解密后的属性流装载到props中  
                    if(fileEncoding != null)...{  
                        propertiesPersister.load(props,  
        new InputStreamReader(is,fileEncoding));  
                    }else...{  
                        propertiesPersister.load(props ,is);  
                    }  
                } finally ...{  
                    if (is != null)  
                        is.close();  
                }  
            }  
        }  
    }  
    }  
}  

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.Key;
import java.util.Properties;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.core.io.Resource;
import org.springframework.util.DefaultPropertiesPersister;
import org.springframework.util.PropertiesPersister;
public class DecryptPropertyPlaceholderConfigurer
        extends PropertyPlaceholderConfigurer ...{
    private Resource[] locations;   //① 重新定义父类中的这个同名属性
    private Resource keyLocation; //② 用于指定密钥文件
    public void setKeyLocation(Resource keyLocation) ...{
        this.keyLocation = keyLocation;
    }
    public void setLocations(Resource[] locations) ...{
        this.locations = locations;
    }
    public void loadProperties(Properties props) throws IOException ...{
        if (this.locations != null) ...{
            PropertiesPersister propertiesPersister = new DefaultPropertiesPersister();
            for (int i = 0; i < this.locations.length; i++) ...{
                Resource location = this.locations[i];
                if (logger.isInfoEnabled()) ...{
                    logger.info("Loading properties file from " + location);
                }
                InputStream is = null;
                try ...{
                    is = location.getInputStream();
                        //③ 加载密钥
                    Key key = DESEncryptUtil.getKey(keyLocation.getInputStream());
                    //④ 对属性文件进行解密
is = DESEncryptUtil.doDecrypt(key, is);
//⑤ 将解密后的属性流装载到props中
                    if(fileEncoding != null)...{
                        propertiesPersister.load(props,
        new InputStreamReader(is,fileEncoding));
                    }else...{
                        propertiesPersister.load(props ,is);
                    }
                } finally ...{
                    if (is != null)
                        is.close();
                }
            }
        }
    }
    }
}

    对locations指定的属性文件流数据进行额外的解密工作,解密后再装载到props中。比起PropertyPlaceholderConfigurer,我们只做了额外的一件事:装载前对属性资源进行解密。

    在代码清单 2的③和④处,我们使用了一个DES解密的工具类对加密的属性文件流进行解密。
    对文件进行对称加密的算法很多,一般使用DES对称加密算法,因为它速度很快,破解困难,DESEncryptUtil不但提供了DES解密功能,还提供了DES加密的功能,因为属性文件在部署前必须经常加密:
    代码清单 3 加密解密工具类

Java代码
public class DESEncryptUtil ...{  
    public static Key createKey() throws NoSuchAlgorithmException {//创建一个密钥  
        Security.insertProviderAt(new com.sun.crypto.provider.SunJCE(), 1);  
        KeyGenerator generator = KeyGenerator.getInstance("DES");  
        generator.init(new SecureRandom());  
        Key key = generator.generateKey();  
        return key;  
    }  
    public static Key getKey(InputStream is) {  
        try ...{  
            ObjectInputStream ois = new ObjectInputStream(is);  
            return (Key) ois.readObject();  
        } catch (Exception e) ...{  
            e.printStackTrace();  
            throw new RuntimeException(e);  
        }  
    }  
    private static byte[] doEncrypt(Key key, byte[] data) {//对数据进行加密  
        try {  
            Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");  
            cipher.init(Cipher.ENCRYPT_MODE, key);  
            byte[] raw = cipher.doFinal(data);  
            return raw;  
        } catch (Exception e) {  
            e.printStackTrace();  
            throw new RuntimeException(e);  
        }  
    }  
    public static InputStream doDecrypt(Key key, InputStream in) {//对数据进行解密  
        try {  
            Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");  
            cipher.init(Cipher.DECRYPT_MODE, key);  
            ByteArrayOutputStream bout = new ByteArrayOutputStream();  
            byte[] tmpbuf = new byte[1024];  
            int count = 0;  
            while ((count = in.read(tmpbuf)) != -1) {  
                bout.write(tmpbuf, 0, count);  
                tmpbuf = new byte[1024];  
            }  
            in.close();  
            byte[] orgData = bout.toByteArray();  
            byte[] raw = cipher.doFinal(orgData);  
            ByteArrayInputStream bin = new ByteArrayInputStream(raw);  
            return bin;  
        } catch (Exception e) {  
            e.printStackTrace();  
            throw new RuntimeException(e);  
        }  
    }  
    public static void main(String[] args) throws Exception {//提供了Java命令使用该工具的功能  
        if (args.length == 2 && args[0].equals("key")) {// 生成密钥文件  
            Key key = DESEncryptUtil.createKey();  
            ObjectOutputStream oos = new ObjectOutputStream(  
                    new FileOutputStream(args[1]));  
            oos.writeObject(key);  
            oos.close();  
            System.out.println("成功生成密钥文件。");  
        } else if (args.length == 3 && args[0].equals("encrypt")) {//对文件进行加密  
            File file = new File(args[1]);  
            FileInputStream in = new FileInputStream(file);  
            ByteArrayOutputStream bout = new ByteArrayOutputStream();  
            byte[] tmpbuf = new byte[1024];  
            int count = 0;  
            while ((count = in.read(tmpbuf)) != -1) {  
                bout.write(tmpbuf, 0, count);  
                tmpbuf = new byte[1024];  
            }  
            in.close();  
            byte[] orgData = bout.toByteArray();  
            Key key = getKey(new FileInputStream(args[2]));  
            byte[] raw = DESEncryptUtil.doEncrypt(key, orgData);  
            file = new File(file.getParent() + "\\en_" + file.getName());  
            FileOutputStream out = new FileOutputStream(file);  
            out.write(raw);  
            out.close();  
            System.out.println("成功加密,加密文件位于:"+file.getAbsolutePath());  
        } else if (args.length == 3 && args[0].equals("decrypt")) {//对文件进行解密  
            File file = new File(args[1]);  
            FileInputStream fis = new FileInputStream(file);  
            Key key = getKey(new FileInputStream(args[2]));  
            InputStream raw = DESEncryptUtil.doDecrypt(key, fis);  
            ByteArrayOutputStream bout = new ByteArrayOutputStream();  
            byte[] tmpbuf = new byte[1024];  
            int count = 0;  
            while ((count = raw.read(tmpbuf)) != -1) {  
                bout.write(tmpbuf, 0, count);  
                tmpbuf = new byte[1024];  
            }  
            raw.close();  
            byte[] orgData = bout.toByteArray();  
            file = new File(file.getParent() + "\\rs_" + file.getName());  
            FileOutputStream fos = new FileOutputStream(file);  
            fos.write(orgData);  
            System.out.println("成功解密,解密文件位于:"+file.getAbsolutePath());  
        }  
    }  
}    

解密工作主要涉及到两个类Cipher和Key,前者是加密器,可以通过init()方法设置工作模式和密钥,在这里,我们设置为解密工作模式:Cipher.DECRYPT_MODE。Cipher通过doFinal()方法对字节数组进行加密或解密。
要完成属性文件的加密工作,首先,必须获取一个密钥文件,然后才能对明文的属性文件进行加密。如果需要调整属性文件的信息,你必须执行相反的过程,即用密钥对加密后的属性文件进行解密,调整属性信息后,再将其加密。
DESEncryptUtil 工具类可以完成以上所提及的三个工作:
? 生成一个密钥文件
java com.baobaotao.DESEncryptUtil key D:\key.dat
第一个参数为key,表示创建密钥文件,第二个参数为生成密钥文件的保存地址。
? 用密钥文件对属性文件进行加密
java com.baobaotao.DESEncryptUtil encrypt d:\test.properties d:\key.dat
第一个参数为encrypt,表示加密,第二个参数为需要加密的属性文件,第三个参数为密钥文件。如果加密成功,将生成en_test.properties的加密文件。
? 用密钥文件对加密后的属性文件进行解密
java com.baobaotao.DESEncryptUtil decrypt d:\test.properties d:\key.dat
第一个参数为decrypt,表示解密,第二个参数为需要解密的属性文件,第三个参数为密钥文件。如果加密成功,将生成rs_test.properties的解密文件。

    在Spring中配置加密属性文件
    假设我们通过DESEncryptUtil 工具类创建了一个key.bat密钥,并对car.properties属性进行加密,生成加密文件en_car.properties。下面,我们通过 DecryptPropertyPlaceholderConfigurer增强类进行配置,让Spring容器支持加密的属性文件:
    假设我们通过DESEncryptUtil 工具类创建了一个key.bat密钥,并对car.properties属性进行加密,生成加密文件en_car.properties。下面,我们通过 DecryptPropertyPlaceholderConfigurer增强类进行配置,让Spring容器支持加密的属性文件:

Java代码
<bean class="com.baobaotao.place.DecryptPropertyPlaceholderConfigurer"> ①  
    <property name="locations">  
        <list>  
            <value>classpath:com/baobaotao/en_car.properties</value>  
        </list>  
    </property>  
    <property name="keyLocation" value="classpath:com/baobaotao/key.dat" />  
    <property name="fileEncoding" value="utf-8" />  
</bean>  
<bean id="car" class="com.baobaotao.place.Car"> ②  
    <property name="brand" value="${brand}" />  
    <property name="maxSpeed" value="${maxSpeed}" />  
    <property name="price" value="${price}" />  
</bean>  
<bean class="com.baobaotao.place.DecryptPropertyPlaceholderConfigurer"> ①
    <property name="locations">
        <list>
            <value>classpath:com/baobaotao/en_car.properties</value>
        </list>
    </property>
    <property name="keyLocation" value="classpath:com/baobaotao/key.dat" />
    <property name="fileEncoding" value="utf-8" />
</bean>
<bean id="car" class="com.baobaotao.place.Car"> ②
    <property name="brand" value="${brand}" />
    <property name="maxSpeed" value="${maxSpeed}" />
    <property name="price" value="${price}" />
</bean>

    注意①处的配置,我们使用自己编写的DecryptPropertyPlaceholderConfigurer替代Spring的 PropertyPlaceholderConfigurer,由于前者对属性文件进行了特殊的解密处理,因此②处的car Bean也可以引用到加密文件en_car.properties中的属性项。

    小结
    要Spring配置时,将一些重要的信息独立到属性文件中是比较常见的做法,Spring只支持明文存放的属性文件,在某些场合下,我们可以希望对属性文件加密保存,以保证关键信息的安全。通过扩展PropertyPlaceholderConfigurer,在属性文件流加载后应用前进行解密就可以很好地解决这个问题了。
1:
java org/evolve/test/encryptor/DESEncryptUtil key jdbc.dat
成功生成密钥文件。
2:
java org/evolve/test/encryptor/DESEncryptUtil encrypt jdbc.properties jdbc.dat
成功加密,加密文件位于:/home/yaoyu/workspace/myweb/WebContent/WEB-INF/classes/en_jdbc.properties
3:
java org/evolve/test/encryptor/DESEncryptUtil decrypt en_jdbc.properties jdbc.dat
成功解密,解密文件位于:/home/yaoyu/workspace/myweb/WebContent/WEB-INF/classes/rs_en_jdbc.properties
分享到:
评论

相关推荐

    在Spring中使用加密外部属性文件

    在Spring框架中,属性文件是配置关键信息的常用...通过这种方式,你可以在Spring中安全地使用外部属性文件,同时满足系统的安全需求。这种做法不仅可以提高应用的可维护性和部署效率,还能保护敏感信息,降低安全风险。

    SSH笔记-通过property-placeholder使用外部属性文件

    通过以上步骤,我们可以利用Spring4的`property-placeholder`特性,将配置信息从主配置文件中分离出来,存储在外部属性文件中,使得配置管理更加灵活和安全。这一实践对于大型项目来说至关重要,因为它提高了代码的...

    SpringCloud中文文档

    引导上下文负责从外部源加载配置属性,还解密本地外部配置文件中的属性。 在使用 Spring Cloud 时,需要注意到引导上下文和主应用程序上下文的区别,引导上下文使用与主应用程序上下文不同的外部配置约定。可以通过...

    jasypt-spring-boot使用说明

    jasypt-spring-boot是Jasypt为Spring Boot定制的一个集成库,它可以自动配置Spring Boot的环境,使得我们可以在配置文件中使用加密的属性值。通过简单的步骤,我们就可以将敏感信息如数据库密码、API密钥等加密存储...

    jasypt-spring-boot-starter 3.0.5依赖的pom及jar

    在实际使用中,开发者需要在自己的POM文件中添加jasypt-spring-boot-starter的依赖,然后在配置文件(如application.properties或application.yml)中声明加密的属性,并指定相应的密钥。这样,Spring Boot在启动时...

    Spring Cloud dalston 中文文档 参考手册

    Ribbon是客户端负载均衡器,通过在Eureka客户端中使用Ribbon,可以实现服务间的负载均衡。Ribbon支持自定义配置,以适应不同的负载均衡策略。 ### 声明式REST客户端与路由器 Feign是一个声明式REST客户端,简化了...

    Spring中PropertyPlaceholderConfigurer的使用

    使用 PropertyPlaceholderConfigurer 需要首先在 Spring 配置文件中定义一个 bean,例如: ```xml &lt;bean id="propertyConfigurerForAnalysis" class="org.springframework.beans.factory.config....

    spring cloud中文文档

    9. **客户端负载平衡器Ribbon**:如何加入Ribbon,自定义Ribbon客户端,使用属性自定义Ribbon客户端,以及在Eureka中使用Ribbon的例子。 10. **声明性REST客户端Feign**:如何加入Feign,覆盖Feign默认值,手动创建...

    Spring Cloud 中文文档

    13. 外部配置Archaius:解释了如何在应用程序中集成Archaius作为外部配置库,以支持动态配置更新。 14. 指标系统:介绍了多种度量指标后端,如Spectator、Servo和Atlas的集成方法,以及如何重试失败的请求。 15. ...

    Spring Cloud Dalston 中文文档 参考手册 中文版.pdf

    通过引导上下文(Bootstrap Context),SpringCloud应用程序能够从外部配置源加载配置属性,并且能够解密外部配置文件中的属性。Bootstrap属性相较于应用程序中的常规配置具有更高的优先级,并且默认情况下不会被...

    Spring Cloud 中文文档 参考手册 中文版2018

    Spring Cloud Stream应用模型中涉及到了生产者、消费者、分区支持以及绑定器(Binder)的使用。Binder SPI提供了与不同消息中间件的绑定器的连接能力。消息通道(Channel)是Spring Cloud Stream的核心概念,它用于...

    spring读取properties

    在Spring框架中,读取和使用...在Spring的配置文件中,首先需要定义一个`PropertyPlaceholderConfigurer` bean,这是Spring用来解析Properties文件并将其值注入到其他bean中的关键组件。如示例所示: ```xml ...

    Spring Cloud 中文文档.pdf

    - **改变引导位置 Properties**:在 Spring Cloud 中,可以通过改变引导文件的位置来调整应用程序的配置方式。 - **覆盖远程 Properties 的值**:Spring Cloud 支持从远程服务获取配置信息,并允许在本地覆盖这些...

    SpringCloud.pdf

    值得注意的是,如果遇到“非法密钥大小”的异常,可能需要在 JDK 的 lib/security 目录下安装 Java 加密扩展(JCE)无限强度策略文件,这在不同版本的 JDK 下有不同的处理方式。 Spring Cloud 以 Apache 2.0 许可证...

    Spring Cloud Finchley.RELEASE 中文 参考手册 中文文档

    Spring Cloud应用程序通过创建一个名为“bootstrap”的应用程序上下文来从外部源加载配置属性,并负责解密本地外部配置文件中的属性。引导属性默认以高优先级加载,防止本地配置覆盖。 #### 引导上下文(Bootstrap ...

    Spring Cloud (Dalston)中文文档

    Spring Cloud应用程序使用“引导”上下文启动,它是主应用程序的父上下文,负责加载外部配置属性并解密属性。Bootstrap属性优先级高于常规应用属性,它们使用不同的配置文件约定,如`bootstrap.yml`和`application....

    spring cloud中文版【Spring Cloud Config】--spring cloud中文文档.pdf

    客户端示例部分会介绍如何在Spring应用中使用Spring Cloud Config客户端,通过配置客户端引入配置服务器的信息,并与之进行通信以获取配置信息。 **配置服务Spring Cloud Config Server** 配置服务端是整个Spring ...

    Spring.3.x企业应用开发实战(完整版).part2

    12.2 在Spring中使用Hibernate 12.2.1 配置SessionFactory 12.2.2 使用HibernateTemplate 12.2.3 处理LOB类型数据 12.2.4 添加Hibernate事件监听器 12.2.5 使用原生Hibernate API 12.2.6 使用注解配置 12.2.7 事务...

    【SpringBoot探索三】添加配置文件参考案例

    - **@ConfigurationProperties**:对于复杂的配置数据,可以使用`@ConfigurationProperties`注解创建一个Java Bean,将配置文件中的多条属性映射到Bean的字段。 4. **配置文件加密**: - **Jasypt**:在Spring ...

    Spring Cloud Dalston 中文文档 参考手册 中文版 带书签

    其中,引导上下文是SpringCloud应用程序的特殊上下文,它负责从外部源加载配置属性,并解密本地外部配置文件中的属性。引导上下文具有高优先级,不能被本地配置覆盖。SpringCloud的配置通常依赖于bootstrap.yml文件...

Global site tag (gtag.js) - Google Analytics