前言
开发中会把一些属性配置放在properties,以方便进行管理,但是如果相关数据未进行加密,便可能导致一些私密数据暴露,比如数据库的用户名和密码.本文章主要讲解两种方式对数据库配置文件进行加密.
一种是使用druid自带的配置,该方式使用简便,但是通用性不强,只适用于数据库文件加密.另一种是使用java编码解决.该方式开发较复杂,但是通用性强.
蓝莓商城项目cms.web使用的是druid方式进行加密处理,os.web使用的是Java编码方式进行加密处理.更多的详情请自行查看.
常规处理-不加密
数据库properties配置文件
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/lanmei?characterEncoding=utf-8
jdbc.username=root
jdbc.password=xxxxxxxxx
jdbc.pool.init=5
jdbc.pool.minIdle=5
jdbc.pool.maxActive=10
jdbc.maxWait=1000
jdbc.timeBetweenEvictionRunsMillis=500
jdbc.minEvictableIdleTimeMillis=40000
jdbc.testSql=show tables;
xml引入properties配置文件
<!-- 引入数据库配置文件 -->
<context:property-placeholder location="classpath:mysqljdbc.properties" />
xml配置文件里面使用”${jdbc.driver}”方式调用
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 数据源驱动类可不写,Druid默认会自动根据URL识别DriverClass -->
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<!-- 基本属性 url、user、password -->
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<!-- 配置初始化大小、最小、最大 -->
<property name="initialSize" value="${jdbc.pool.init}" />
<property name="minIdle" value="${jdbc.pool.minIdle}" />
<property name="maxActive" value="${jdbc.pool.maxActive}" />
<!-- 配置获取连接等待超时的时间 -->
<property name="maxWait" value="${jdbc.maxWait}" />
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}" />
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}" />
<property name="validationQuery" value="${jdbc.testSql}" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<!-- 打开PSCache,并且指定每个连接上PSCache的大小(Oracle使用) -->
<property name="poolPreparedStatements" value="true" />
<property name="maxPoolPreparedStatementPerConnectionSize" value="20" />
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="stat" />
</bean>
这种方式不对配置进行加密,很容易造成安全问题.
使用druid的配置进行加密处理
如果项目使用druid做所谓数据库连接池,那么可以使用其ConfigFilter进行配置.这里只讲解当配置文件从本地中读取时的加密处理,更多方式请参考druid官方文档
我使用的druid版本为1.0.29,放置在.m2/repository/com/alibaba/druid/1.0.29/
目录下.
进入该目录
cd .m2/repository/com/alibaba/druid/1.0.29/
再运行目录下的com.alibaba.druid.filter.config.ConfigTools类,参数为你的数据库密码.
java -cp druid-1.0.29.jar com.alibaba.druid.filter.config.ConfigTools 563739007
druid使用的是不可逆加密方式,因此会有私钥和公钥.
privateKey:私钥;
publicKey:公钥;
password:加密后的密码
privateKey:MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAw3NrAA8X/2MGuvf8HGn2VlOjGW7qkIh3NFv9jtYsyHtsqtReVuUAfxPZM4sPNu72qcxEcVavekAz7Gb58n3PlwIDAQABAkB6LQDa9ZRzsWw4neG7xUUWa4vNzzbTiGqzkTlr+1fdLapgm/1Pb1il8Z/Plxnl7wdl0HHq02TbyZEa17fgu96JAiEA5SHvQcarJdBH7ResAKcMa/2WNmYSTCvy1BMwICNcfkMCIQDaXm5h4un8cafC4iG5Q4+Y+KL2gkN72bz+VmzbF/NWHQIhAKZ0opWMOCU+TCJHgiLvOCzzij52pHBFtSCv19RhG/51AiEAs5Fbq9rxFspPbg6ONM690skDGTrdS4ctxuhC85eqXnECIF9WCpnQp+A3M5EkYj4yG+uCeCvIiFLkvZW+DlSJa6SX
publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMNzawAPF/9jBrr3/Bxp9lZToxlu6pCIdzRb/Y7WLMh7bKrUXlblAH8T2TOLDzbu9qnMRHFWr3pAM+xm+fJ9z5cCAwEAAQ==
password:g6n3goSvd0ggKOlZ+KEVXsfc84/KWjvOd6t1VLUr/cOFW3k4E3NN1PtzR5e3ImRgZWktN0NLDIZIXGZjQ+IiLg==
在数据库配置文件中添加jdbc.publicKey属性,并将上述的publicKey和password填入.由于每次生成的数据密钥不一样,因此上面生成的密钥和图中的还有下面的不一样,不必拘泥于这个.
jdbc.publicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIe5K9tyOgml4FWfhmhLtvGIJohv9QUk0EJDrAcqxfjEgZJOvaeFEU0eN+WhPGM6KOt8SabL45Jo823D0NCwGtUCAwEAAQ==
jdbc.password=hcqgyt5Q3IanwC8BIz6XZM10/El+vwcPs0CAcaZFzTvw7mFptiANbLv2WoIHFtMc76BCadXBh4VooOFCdidMFg==
修改数据库xml配置文件.
只要在上面的配置文件基础上修改这两个地方:
1.filters添加值config,stat是和监控统计拦截相关,两个属性值之间使用逗号进行隔开
2.配置属性connectionProperties,如下图,这里就需要jdbc.publicKey了
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<!-- 配置监控统计拦截的filters -->
<property name="filters" value="config,stat" />
<property name="connectionProperties" value="config.decrypt=true;config.decrypt.key=${jdbc.publicKey}" />
</bean>
至此,就已经配置完成.实现数据库密码加密功能.
由于该方式只适用于使用druid作为数据库连接池的场景,不能适用于其他类型的配置文件,因此不具有通用性.
使用Java编码进行加密处理.
之前写过一篇文章创建SqlSessionFactory时对数据库配置中的加密用户名和密码进行解密操作示例,该方式是通过读取properties文件中的配置信息,进行加密后再写入配置文件.
但是Spring 给我们提供了一种更好的方式,使用PropertyPlaceholderConfigurer类来实现对配置文件的修改.
其主要的原理在是。Spring容器初始化的时候,会读取xml或者annotation对Bean进行初始化。初始化的时候,这个PropertyPlaceholderConfigurer会拦截Bean的初始化,初始化的时候会对配置的${pname}进行替换,根据我们Properties中配置的进行替换。从而实现表达式的替换操作 。
PropertyPlaceholderConfigurer继承自PropertyResourceConfigurer类,后者有几个protected方法,用于在属性使用之前对属性列表中的属性进行转换.
//配置文件中的所有属性都在props,可对所有的属性值进行转换
protected void convertProperties(java.util.Properties props)
//在加载属性配置文件并读取配置属性时都会调用该方法,可以对所有的值进行转换,返回的是新的propertyValue
protected java.lang.String convertProperty(java.lang.String propertyName,
java.lang.String propertyValue)
//和上面的类似,不过传入的是propertyValue,没有传入属性名
protected java.lang.String convertPropertyValue(java.lang.String originalValue)
三个方法都是空的,也就是没有对属性值进行任何的修改,子类可以扩展该类,实现属性值修改.
一般我们xml配置文件要使用properties文件中的属性,就需要这样用”${xxxx}”
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
但是其使用前必须引入配置文件,存在两种方式.
方式1:
<!-- 引入数据库配置文件 -->
<context:property-placeholder location="classpath:mysqljdbc.properties" />
方式2:
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"
p:location="classpath:mysqljdbc.properties"
p:fileEncoding="utf-8"/>
但是两种方式只能选择其中之一,为了实现配置文件属性加密功能,选用方式2.
修改属性值处理类
创建子类EnctryptPropertyPlaceholderConfigurer继承PropertyPlaceholderConfigurer,实现对属性的修改.
处理流程:调用函数-->检查该属性是否需要解密-->(需要解密)调用加密工具类对属性值进行解密-->获取到解密后的数据(为原始数据,比如用户名或者密码等)-->返回解密后的数据-->Spring会自动将该属性更新为新的属性值.
package org.lanmei.os.utils.property;
import org.lanmei.os.utils.des.DESUtils;
import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer;
import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
public class EnctryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer {
//定义需要解密的属性
private String[] enctryptPropertyValue= {"jdbc.username","jdbc.password"};
/**
* 在加载属性配置文件并读取配置属性时都会调用该方法,可以对所有的值进行转换,返回的是新的propertyValue
*/
@Override
protected String convertProperty(String propertyName, String propertyValue) {
System.out.println(propertyName + " = " + propertyValue );
if(isEnctryptPropertyValue(propertyName)) {
String decryResult = null;
try {
//解密操作
decryResult = DESUtils.decrypt(propertyValue);
System.out.println("解密后:"+decryResult);
} catch (Exception e1) {
e1.printStackTrace();
}
//返回解密后的属性值
return (decryResult);
}
return propertyValue;
}
/**
* 判断是否需要解密
* @param value
* @return
*/
private boolean isEnctryptPropertyValue(String value){
for(String propertyValue:enctryptPropertyValue) {
if(propertyValue.equals(value)) {
return true;
}
}
return false;
}
}
加密工具类
加密算法使用的是DES,其是一种对称加密算法.
package org.lanmei.os.utils.des;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
/**
* DFS加密工具类
* @author lgj
*
*/
public class DESUtils {
//任意值,必须为8位,否则报错
private static String secKey = "12345678";
public static void main(String args[]) {
//这段是用于测试
if(false) {
//待加密内容
String encryptValue = "cryptology";
System.out.println("加密的数据为 = " + encryptValue);
//执行加密
String enresult = DESUtils.encrypt(encryptValue);
try {
//执行解密
String decryResult = DESUtils.decrypt(enresult);
System.out.println("解密后:"+decryResult);
} catch (Exception e1) {
e1.printStackTrace();
}
}
//这段是用于java指令执行时获取加密数据
if(true) {
if((args == null) || (args.length < 1)) {
System.out.println("请输入需要加密的字符");
}
else {
System.out.println("正在执行加密....");
System.out.println("+---------------------------+");
for(String arg:args) {
System.out.println("加密的字符为 = " + arg);
String enresult = DESUtils.encrypt(arg);
System.out.println("加密完成");
System.out.println("加密后的字符为: " + enresult);
System.out.println("进行解密测试....");
String decryResult = null;
try {
decryResult = DESUtils.decrypt(enresult);
System.out.println("解密后:"+decryResult);
} catch (Exception e1) {
e1.printStackTrace();
}
if(decryResult.equals(arg)) {
System.out.println("加密成功");
}
else {
System.out.println("加密失败");
}
System.out.println("+---------------------------+");
}
}
}
}
/**
* 加密
* @param datasource byte[]
* @param password String
* @return byte[]
*/
public static String encrypt(String encryptValue) {
Encoder encoder = Base64.getEncoder();
try{
SecureRandom random = new SecureRandom();
DESKeySpec desKey = new DESKeySpec(secKey.getBytes());
//创建一个密匙工厂,然后用它把DESKeySpec转换成
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey securekey = keyFactory.generateSecret(desKey);
//Cipher对象实际完成加密操作
Cipher cipher = Cipher.getInstance("DES");
//用密匙初始化Cipher对象,ENCRYPT_MODE用于将 Cipher 初始化为加密模式的常量
cipher.init(Cipher.ENCRYPT_MODE, securekey, random);
//现在,获取数据并加密
//正式执行加密操作
byte[] encryptByte = cipher.doFinal(encryptValue.getBytes());
return encoder.encodeToString(encryptByte);
}catch(Throwable e){
e.printStackTrace();
}
return null;
}
/**
*
* @param str
* @return
* @throws Exception
*/
public static String decrypt(String decryptValue) throws Exception {
Decoder decoder = Base64.getDecoder();
byte[] src = decoder.decode(decryptValue);
// DES算法要求有一个可信任的随机数源
SecureRandom random = new SecureRandom();
// 创建一个DESKeySpec对象
DESKeySpec desKey = new DESKeySpec(secKey.getBytes());
// 创建一个密匙工厂
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");//返回实现指定转换的 Cipher 对象
// 将DESKeySpec对象转换成SecretKey对象
SecretKey securekey = keyFactory.generateSecret(desKey);
// Cipher对象实际完成解密操作
Cipher cipher = Cipher.getInstance("DES");
// 用密匙初始化Cipher对象
cipher.init(Cipher.DECRYPT_MODE, securekey, random);
// 真正开始解密操作
byte[] decryptByte = cipher.doFinal(src);
return new String(decryptByte);
}
}
获取加密后的数据
加密工具类中有一个main()函数,可以通过其获取加密后的数据
编译上面的类,进入到工程存放类的路径下,运行该工具类,并设置传入的参数为需要加密的数据,比如name和password.
可以一次性加密多个属性值,属性值之间空格隔开这里对用户名和密码进行加密.执行后便可以获取到加密后的字符.
java org/lanmei/os/utils/des/DESUtils root 563739007
输出
正在执行加密....
+---------------------------+
加密的字符为 = root
加密完成
加密后的字符为: FyZA3VCAdiU=
进行解密测试....
解密后:root
加密成功
+---------------------------+
加密的字符为 = 563739007
加密完成
加密后的字符为: LfNyV2NqhLbXICUpTIAFcA==
进行解密测试....
解密后:563739007
加密成功
+---------------------------+
修改数据库配置文件
将加密后的字符替换原来的用户名root和原始密码
jdbc.username=FyZA3VCAdiU=
jdbc.password=LfNyV2NqhLbXICUpTIAFcA==
修改xml配置文件
这里创建的bean更改为EnctryptPropertyPlaceholderConfigurer类
<bean class="org.lanmei.os.utils.property.EnctryptPropertyPlaceholderConfigurer"
p:location="classpath:mysqljdbc.properties"
p:fileEncoding="utf-8"/>
完成.