Druid的功能
1、替换DBCP和C3P0。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
2、可以监控数据库访问性能,Druid内置提供了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,这对于线上分析数据库访问性能有帮助。
3、数据库密码加密。直接把数据库密码写在配置文件中,这是不好的行为,容易导致安全问题。DruidDruiver和DruidDataSource都支持PasswordCallback。
4、SQL执行日志,Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和JdkLog,你可以按需要选择相应的LogFilter,监控你应用的数据库访问情况。
5、扩展JDBC,如果你要对JDBC层有编程的需求,可以通过Druid提供的Filter机制,很方便编写JDBC层的扩展插件。
其中第三条说出了本博客的一个目的,详细过程如下:
1、首先配置Druid的数据库连接池
<!--数据源加密操作-->
<bean id="dbPasswordCallback" class="com.xuliugen.db.config.DBPasswordCallback" lazy-init="true"/>
<bean id="statFilter" class="com.alibaba.druid.filter.stat.StatFilter" lazy-init="true">
<property name="logSlowSql" value="true"/>
<property name="mergeSql" value="true"/>
</bean>
<!-- 数据库连接 -->
<bean id="readDataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close" init-method="init" lazy-init="true">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url1}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<!-- 初始化连接大小 -->
<property name="initialSize" value="${initialSize}"/>
<!-- 连接池最大数量 -->
<property name="maxActive" value="${maxActive}"/>
<!-- 连接池最小空闲 -->
<property name="minIdle" value="${minIdle}"/>
<!-- 获取连接最大等待时间 -->
<property name="maxWait" value="${maxWait}"/>
<!-- -->
<property name="defaultReadOnly" value="true"/>
<property name="proxyFilters">
<list>
<ref bean="statFilter"/>
</list>
</property>
<property name="filters" value="${druid.filters}"/>
<property name="connectionProperties" value="password=${password}"/>
<property name="passwordCallback" ref="dbPasswordCallback"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="timeBetweenLogStatsMillis" value="60000"/>
<!-- 配置一个连接在池中最小生存的时间,单位是毫秒 -->
<property name="minEvictableIdleTimeMillis" value="${minEvictableIdleTimeMillis}"/>
<!-- 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 -->
<property name="timeBetweenEvictionRunsMillis" value="${timeBetweenEvictionRunsMillis}"/>
</bean>
其中要注意的是:
<bean id="dbPasswordCallback" class="com.xuliugen.db.config.DBPasswordCallback" lazy-init="true"/>
<property name="passwordCallback" ref="dbPasswordCallback"/>
2、创建DruidPasswordCallback的子类如下:
这里的DBPasswordCallback 是继承com.alibaba.druid.util.DruidPasswordCallback 的,重写的是DruidPasswordCallback 的setProperties方法,在setProperties方法中使用了setPassword(password.toCharArray());这个方法,setPassword是DruidPasswordCallback 的父类中的一个方法。
一个代码追踪过程:
1、使用com.alibaba.druid.filter.config.ConfigTools
提供的加密和解密方法
从源代码中可以看出,ConfigTools加密和解密使用了默认的公钥和私钥,这里我们创建自己的公钥和私钥。
2、使用RSA公钥和私钥,生成一对公钥和私钥的工具类:
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.HashMap;
import java.util.Map;
/**
* Created by xuliugen on 2017/6/25.
*/
public class RSAKeysUtil {
public static final String KEY_ALGORITHM = "RSA";
public static final String SIGNATURE_ALGORITHM = "MD5withRSA";
private static final String PUBLIC_KEY = "RSAPublicKey";
private static final String PRIVATE_KEY = "RSAPrivateKey";
public static void main(String[] args) {
Map<String, Object> keyMap;
try {
keyMap = initKey();
String publicKey = getPublicKey(keyMap);
System.out.println(publicKey);
String privateKey = getPrivateKey(keyMap);
System.out.println(privateKey);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String getPublicKey(Map<String, Object> keyMap) throws Exception {
Key key = (Key) keyMap.get(PUBLIC_KEY);
byte[] publicKey = key.getEncoded();
return encryptBASE64(key.getEncoded());
}
public static String getPrivateKey(Map<String, Object> keyMap) throws Exception {
Key key = (Key) keyMap.get(PRIVATE_KEY);
byte[] privateKey = key.getEncoded();
return encryptBASE64(key.getEncoded());
}
public static byte[] decryptBASE64(String key) throws Exception {
return (new BASE64Decoder()).decodeBuffer(key);
}
public static String encryptBASE64(byte[] key) throws Exception {
return (new BASE64Encoder()).encodeBuffer(key);
}
public static Map<String, Object> initKey() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String, Object> keyMap = new HashMap<String, Object>(2);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
}
结果如下:
上边是公钥下边是私钥。
3、使用私钥对明文密码进行加密
package com.dlt.server.core.db.password;
import com.alibaba.druid.filter.config.ConfigTools;
import org.junit.Test;
/**
* Created by xuliugen on 2017/6/25.
*/
public class ConfigToolsDemo {
//上述生成的私钥
private static final String PRIVATE_KEY_STRING = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJ4xPqg4LsjvKlgiokq66ZJAqs90EN/XGwkZHpsWJ+OAAuQwYYYh6VhpBKzC5nNLJBTaPcegmdejbJMHONTQKpStX93G5q0CQ229ghvGdeQU8m/bmZfXnxYG1qJWKzwVlxtfOiFn9XFyMv2C9/VOiavHywx6Skue45QbLORc6zUTAgMBAAECgYBuoFOIAlo9bHu5TOcfyZykCZMqJqnST7R5ZVaw8AqPHytmdqsMyVRM3oxFYLsWL5sY9hI0M4zCb2fzXh6RPM45N2QusjF3joSa/QHFZWLD1W5vry0aIPWUqVLt5VqOnKimX36ivz6snORfzvXoOMVP6mHW1XVYfr2cYvb+gZbIcQJBANE8Stm5UFrArTQN+XsTTC4yo5NgAbuWnG5xqkRuSTuEqbw+wtLifjqa6mL67k6etCqBaM2upJCIlDqsaSsmgdsCQQDBjHIY1C5xkBI939X/fDbDpwqVAIsIWy33BOShHDxnVXvXStSxhRQkVaDgwmjaOlY16evBrSoDY9uYKKpKRAspAkEAzFqeoFcl6/0TLSwY5ePLG7PJnz69coF+9z98lKlCTSccwAZsMZuUvZhgI5wA9Dh8rqcFvR09DQzX+RY7ATHy0QJAZl6JTnaTZf9ElrNYNXwWXx9vqmWSI8ZOJnPRFSGhFSqSiMmMe6QehiVAJQDOgnYOeQ+TYWncadScJfuELimVGQJAX4E0KUf4uWq68Y0qSqu9UhZT8IYGl4gzMMp4Hz8AK74VWGSg6uJMWnDF/Iyk9YmMBisveS9+KqJN8qNIaf/fkg==";
@Test
public void demo() throws Exception {
//密码明文,也就是数据库的密码
String plainText = "123456";
System.out.printf(ConfigTools.encrypt(PRIVATE_KEY_STRING, plainText));
}
}
结果如下:
那我们数据库的密码就应该设置为这个结果:
4、解析密码的时候需要的Callback类
import com.alibaba.druid.util.DruidPasswordCallback;
import org.apache.commons.lang3.StringUtils;
import java.util.Properties;
/**
* 数据库密码回调解密
*/
@SuppressWarnings("serial")
public class DBPasswordCallback extends DruidPasswordCallback {
//上述生成的公钥
public static final String PUBLIC_KEY_STRING = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCeMT6oOC7I7ypYIqJKuumSQKrPdBDf1xsJGR6b
FifjgALkMGGGIelYaQSswuZzSyQU2j3HoJnXo2yTBzjU0CqUrV/dxuatAkNtvYIbxnXkFPJv25mX
158WBtaiVis8FZcbXzohZ/VxcjL9gvf1Tomrx8sMekpLnuOUGyzkXOs1EwIDAQAB";
public void setProperties(Properties properties) {
super.setProperties(properties);
String pwd = properties.getProperty("password");
if (StringUtils.isNotBlank(pwd)) {
try {
//这里的password是将jdbc.properties配置得到的密码进行解密之后的值
//所以这里的代码是将密码进行解密
//TODO 将pwd进行解密;
String password = ConfigTools.decrypt(PUBLIC_KEY_STRING, pwd);
setPassword(password.toCharArray());
} catch (Exception e) {
setPassword(pwd.toCharArray());
}
}
}
// 请使用该方法加密后,把密文写入classpath:/config/jdbc.properties
public static void main(String[] args) {
System.out.println(SecurityUtil.encryptDes("", key));
}
}
其中PasswordCallback是javax.security.auth.callback包下面的,底层安全服务实例化一个 PasswordCallback 并将其传递给 CallbackHandler 的 handle 方法,以获取密码信息。
当然,除了使用上述的方式,自己也可以对应一套加解密方法,只需要在DBPasswordCallback
的 String password = ConfigTools.decrypt(PUBLIC_KEY_STRING, pwd);
替换即可。
3、在jdbc.properties存放自己加密之后的信息
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username=root
#回调DBPasswordCallback解密,这里的密码是你加密之后的密码!!!
password=CuR3P57JAlPQYsf+zXh3YLq3ZN93alr/XLvraCI94GU5c8UQZjV1qRfZsHLyyzjKiNj+ghN68bHNuht36xHHXbQ5oqZuAGNch6g+VV8/d2ySC8gczhWIXA5SK4v6KK/HYTPy/GnR317yy3wqwJ/AqXdRsqmHLkBF53L8ctG7nyQ=
#定义初始连接数
initialSize=20
#定义最大连接数
maxActive=40
#定义最大空闲
maxIdle=20
#定义最小空闲
minIdle=1
#定义最长等待时间
maxWait=60000
注意:2、3过程中密码的设置要确定,加密、解密的最初始密码是要对应的。
4、设置自定义的DruidPasswordCallback
在自己的spring配置文件中加入下边的一句bean配置:
<!--数据源加密操作 这里的class就是第2步中自己创建的-->
<bean id="dbPasswordCallback" class="com.xuliugen.db.config.DBPasswordCallback" lazy-init="true"/>
另外还可以直接继承自Spring提供的PropertyPlaceholderConfigurer,摘录别人一段代码大家参考一下:
public class DecryptPropertyPlaceholderConfigurer extends PropertyPlaceholderConfigurer{
/**
* 重写父类方法,解密指定属性名对应的属性值
*/
@Override
protected String convertProperty(String propertyName,String propertyValue){
if(isEncryptPropertyVal(propertyName)){
return DesUtils.getDecryptString(propertyValue);//调用解密方法
}else{
return propertyValue;
}
}
/**
* 判断属性值是否需要解密,这里我约定需要解密的属性名用encrypt开头
* @param propertyName
* @return
*/
private boolean isEncryptPropertyVal(String propertyName){
if(propertyName.startsWith("encrypt")){
return true;
}else{
return false;
}
}
}