WAS로 Tomcat을 사용하면 DB Connection Pool로 DBCP를 많이들 사용할 것이다.
여기서 제가 한가지 짚고 넘어갈 것은 DBCP 설정을 server.xml에 하든 Context Path XML에 설정을 하든 문제가 되는 부분은 DataBase의 url, username, password 등 DB에 대한 정보가 그대로 파일에 명시된다. 그럼 만약 Web Server가 해킹이 된다면, DB의 연결 정보는 모두 노출되는 보안의 취약점으 발생하게 된다.
WebLogic과 같은 상용 WAS들은 대부분이 DB User의 Password를 암호화하여 저장되어 있다.
그럼 Tomcat에서 DBCP를 사용할 때는 상용 WAS와 같이 설정 정보를 암호화 하여 사용할 수는 없는것인가? 당연히 가능하다. DBCP에 있는 DataSourceFactory를 조금만 변경하면 됩니다.
-- Default DBCP Setting
<Context path="test" docBase="d:/www-root/test/webapp" debug="5" reloadable="true" crossContext="true">
<Resource name="jdbc/testDB" auth="Container" type="javax.sql.DataSource"
factory="org.apache.commons.dbcp.BasicDataSourceFactory"
initialSize="5" maxActive="10" maxIdle="5" maxWait="15000"
username="test" password="1234" driverClassName="oracle.jdbc.driver.OracleDriver"
url="jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1521:ora" validationQuery="SELECT 1 FROM DUAL"
removeAbandoned="false" removeAbandonedTimeout="120" logAbandoned="false" />
</Context>
위 설정은 기본으로 설정 된 DBCP이다. 여기서 factory, username, password, url 부분을 수정하여 DB 접속 정보를 암호화 해볼것이다. 물론 다른 정보들도 암호화가 가능하다.
-- DBCP 설정 정보 암호화하기
일단 먼저 factory를 수정해야 한다. org.apache.commons.dbcp.BasicDataSourceFacroty의 소스를 조금 변경하여 암호화 된 정보를 복호화하여 적용하는 부분이 추가된 새로운 DataSourceFactory를 만든다.
package com.junducki.blog.dbcp;
import java.io.ByteArrayInputStream;
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.goorm.common.security.SeedCipher;
import com.goorm.common.util.StringUtil;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;
/**
* Tomcat의 Resource(DBCP)를 Context Path XML에 설정하여 사용할때, DataBase
* url/username/password 부분을 암호화하여 사용하기 위한 DataSourceFactory
*
* @author jinuk jung, junducki@naver.com
* @version 1.0, 2008. 03. 13
*/
public class EncryptDataSourceFacroty implements ObjectFactory {
protected static final int UNKNOWN_TRANSACTIONISOLATION = -1;
private final static String PROP_DEFAULTAUTOCOMMIT = "defaultAutoCommit";
private final static String PROP_DEFAULTREADONLY = "defaultReadOnly";
private final static String PROP_DEFAULTTRANSACTIONISOLATION = "defaultTransactionIsolation";
private final static String PROP_DEFAULTCATALOG = "defaultCatalog";
private final static String PROP_DRIVERCLASSNAME = "driverClassName";
private final static String PROP_MAXACTIVE = "maxActive";
private final static String PROP_MAXIDLE = "maxIdle";
private final static String PROP_MINIDLE = "minIdle";
private final static String PROP_INITIALSIZE = "initialSize";
private final static String PROP_MAXWAIT = "maxWait";
private final static String PROP_TESTONBORROW = "testOnBorrow";
private final static String PROP_TESTONRETURN = "testOnReturn";
private final static String PROP_TIMEBETWEENEVICTIONRUNSMILLIS = "timeBetweenEvictionRunsMillis";
private final static String PROP_NUMTESTSPEREVICTIONRUN = "numTestsPerEvictionRun";
private final static String PROP_MINEVICTABLEIDLETIMEMILLIS = "minEvictableIdleTimeMillis";
private final static String PROP_TESTWHILEIDLE = "testWhileIdle";
private final static String PROP_PASSWORD = "password";
private final static String PROP_URL = "url";
private final static String PROP_USERNAME = "username";
private final static String PROP_VALIDATIONQUERY = "validationQuery";
private final static String PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED
= "accessToUnderlyingConnectionAllowed";
private final static String PROP_REMOVEABANDONED = "removeAbandoned";
private final static String PROP_REMOVEABANDONEDTIMEOUT = "removeAbandonedTimeout";
private final static String PROP_LOGABANDONED = "logAbandoned";
private final static String PROP_POOLPREPAREDSTATEMENTS = "poolPreparedStatements";
private final static String PROP_MAXOPENPREPAREDSTATEMENTS = "maxOpenPreparedStatements";
private final static String PROP_CONNECTIONPROPERTIES = "connectionProperties";
private final static String[] ALL_PROPERTIES = { PROP_DEFAULTAUTOCOMMIT,
PROP_DEFAULTREADONLY, PROP_DEFAULTTRANSACTIONISOLATION, PROP_DEFAULTCATALOG,
PROP_DRIVERCLASSNAME, PROP_MAXACTIVE, PROP_MAXIDLE, PROP_MINIDLE,
PROP_INITIALSIZE, PROP_MAXWAIT, PROP_TESTONBORROW, PROP_TESTONRETURN,
PROP_TIMEBETWEENEVICTIONRUNSMILLIS, PROP_NUMTESTSPEREVICTIONRUN,
PROP_MINEVICTABLEIDLETIMEMILLIS, PROP_TESTWHILEIDLE, PROP_PASSWORD,
PROP_URL, PROP_USERNAME, PROP_VALIDATIONQUERY,
PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED, PROP_REMOVEABANDONED,
PROP_REMOVEABANDONEDTIMEOUT, PROP_LOGABANDONED,
PROP_POOLPREPAREDSTATEMENTS, PROP_MAXOPENPREPAREDSTATEMENTS,
PROP_CONNECTIONPROPERTIES };
/** logger instance */
protected static Log logger = LogFactory.getLog(GoormDataSourceFacroty.class);
/** DataSource Factory Name */
private static String dsFactoryName = null;
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable environment)
throws Exception {
dsFactoryName = name.toString();
// We only know how to deal with <code>javax.naming.Reference</code>s
// that specify a class name of "javax.sql.DataSource"
if ((obj == null) || !(obj instanceof Reference)) {
return null;
}
Reference ref = (Reference) obj;
if (!"javax.sql.DataSource".equals(ref.getClassName())) {
return null;
}
Properties properties = new Properties();
for (int i = 0; i < ALL_PROPERTIES.length; i++) {
String propertyName = ALL_PROPERTIES[i];
RefAddr ra = ref.get(propertyName);
if (ra != null) {
String propertyValue = ra.getContent().toString();
properties.setProperty(propertyName, propertyValue);
}
}
return createDataSource(properties);
}
public static DataSource createDataSource(Properties properties)
throws Exception {
BasicDataSource dataSource = new BasicDataSource();
String value = null;
StringBuilder trace = null;
value = properties.getProperty(PROP_DRIVERCLASSNAME);
if (value != null) {
dataSource.setDriverClassName(value);
}
value = properties.getProperty(PROP_URL);
if (value != null) {
dataSource.setUrl(decryptDBCPProperty(value));
}
value = properties.getProperty(PROP_USERNAME);
if (value != null) {
dataSource.setUsername(decryptDBCPProperty(value));
}
value = properties.getProperty(PROP_PASSWORD);
if (value != null) {
dataSource.setPassword(decryptDBCPProperty(value));
}
value = properties.getProperty(PROP_DEFAULTAUTOCOMMIT);
if (value != null) {
dataSource.setDefaultAutoCommit(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_DEFAULTREADONLY);
if (value != null) {
dataSource.setDefaultReadOnly(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_DEFAULTTRANSACTIONISOLATION);
if (value != null) {
int level = UNKNOWN_TRANSACTIONISOLATION;
if ("NONE".equalsIgnoreCase(value)) {
level = Connection.TRANSACTION_NONE;
} else if ("READ_COMMITTED".equalsIgnoreCase(value)) {
level = Connection.TRANSACTION_READ_COMMITTED;
} else if ("READ_UNCOMMITTED".equalsIgnoreCase(value)) {
level = Connection.TRANSACTION_READ_UNCOMMITTED;
} else if ("REPEATABLE_READ".equalsIgnoreCase(value)) {
level = Connection.TRANSACTION_REPEATABLE_READ;
} else if ("SERIALIZABLE".equalsIgnoreCase(value)) {
level = Connection.TRANSACTION_SERIALIZABLE;
} else {
try {
level = Integer.parseInt(value);
} catch (NumberFormatException e) {
System.err.println("Could not parse defaultTransactionIsolation: " + value);
System.err.println("WARNING: defaultTransactionIsolation not set");
System.err.println("using default value of database driver");
level = UNKNOWN_TRANSACTIONISOLATION;
}
}
dataSource.setDefaultTransactionIsolation(level);
}
value = properties.getProperty(PROP_DEFAULTCATALOG);
if (value != null) {
dataSource.setDefaultCatalog(value);
}
value = properties.getProperty(PROP_MAXACTIVE);
if (value != null) {
dataSource.setMaxActive(Integer.parseInt(value));
}
value = properties.getProperty(PROP_MAXIDLE);
if (value != null) {
dataSource.setMaxIdle(Integer.parseInt(value));
}
value = properties.getProperty(PROP_MINIDLE);
if (value != null) {
dataSource.setMinIdle(Integer.parseInt(value));
}
value = properties.getProperty(PROP_INITIALSIZE);
if (value != null) {
dataSource.setInitialSize(Integer.parseInt(value));
}
value = properties.getProperty(PROP_MAXWAIT);
if (value != null) {
dataSource.setMaxWait(Long.parseLong(value));
}
value = properties.getProperty(PROP_TESTONBORROW);
if (value != null) {
dataSource.setTestOnBorrow(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_TESTONRETURN);
if (value != null) {
dataSource.setTestOnReturn(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_TIMEBETWEENEVICTIONRUNSMILLIS);
if (value != null) {
dataSource.setTimeBetweenEvictionRunsMillis(Long.parseLong(value));
}
value = properties.getProperty(PROP_NUMTESTSPEREVICTIONRUN);
if (value != null) {
dataSource.setNumTestsPerEvictionRun(Integer.parseInt(value));
}
value = properties.getProperty(PROP_MINEVICTABLEIDLETIMEMILLIS);
if (value != null) {
dataSource.setMinEvictableIdleTimeMillis(Long.parseLong(value));
}
value = properties.getProperty(PROP_TESTWHILEIDLE);
if (value != null) {
dataSource.setTestWhileIdle(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_VALIDATIONQUERY);
if (value != null) {
dataSource.setValidationQuery(value);
}
value = properties.getProperty(PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED);
if (value != null) {
dataSource.setAccessToUnderlyingConnectionAllowed(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_REMOVEABANDONED);
if (value != null) {
dataSource.setRemoveAbandoned(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_REMOVEABANDONEDTIMEOUT);
if (value != null) {
dataSource.setRemoveAbandonedTimeout(Integer.parseInt(value));
}
value = properties.getProperty(PROP_LOGABANDONED);
if (value != null) {
dataSource.setLogAbandoned(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_POOLPREPAREDSTATEMENTS);
if (value != null) {
dataSource.setPoolPreparedStatements(Boolean.valueOf(value).booleanValue());
}
value = properties.getProperty(PROP_MAXOPENPREPAREDSTATEMENTS);
if (value != null) {
dataSource.setMaxOpenPreparedStatements(Integer.parseInt(value));
}
value = properties.getProperty(PROP_CONNECTIONPROPERTIES);
if (value != null) {
Properties p = getProperties(value);
Enumeration e = p.propertyNames();
while (e.hasMoreElements()) {
String propertyName = (String) e.nextElement();
dataSource.addConnectionProperty(propertyName, p.getProperty(propertyName));
}
}
return dataSource;
}
static private Properties getProperties(String propText) throws Exception {
Properties p = new Properties();
if (propText != null) {
p.load(new ByteArrayInputStream(propText.replace(';', '\n').getBytes()));
}
return p;
}
/**
* 암호화 된 DBCP Property를 복호화해 준다.
*
* @param encryptStr String 암호화 된 Config 값
* @return String 복호화 된 Config 값
*/
private static String decryptDBCPProperty(String encryptStr) {
SeedCipher seed = new SeedCipher();
try {
return seed.decryptAsString(Base64.decode(encryptStr), key.getBytes(), "UTF-8");
} catch (UnsupportedEncodingException e) {
return seed.decryptAsString(Base64.decode(encryptStr), key.getBytes());
}
}
}
위의 소스를 보시면 BasicDataSourceFactory가 어떻게 수정되었는지를 금방 알수 있을 것이다. 변경되거나 추가된 부분은 소스 색을 다르게 해 놨다.
decryptDBCPProperty(String encryptStr)은 설정 된 암호화 된 properties를 복호화하기 위해 추가 된 것으로 여기에 암호화는 SEED API를 적용해 놨다. key는 변경하거나 룰을 가져가면 된다. 물론 다른 암호화 알고리즘을 적용해도 된다. 대신 설정 된 암호화 properties와 복호화 하는 부분에는 동일한 키와 알고리즘을 사용해야 한다.
getObjectInstance() 함수의 달라진 부분은 읽어들인 properties를 decryptDBCPProperty()를 이용하여 복호화하여 적용하게끔 변경되었다. url, username, password 부분만 현재는 적용 됨.
그럼 위의 EncryptDataSourceFacroty가 적용 된 DBCP 설정은 어떻게 달라지는가? 위의 DBCP 설정을 변경해 보면
<Context path="test" docBase="d:/www-root/test/webapp" debug="5" reloadable="true" crossContext="true">
<Resource name="jdbc/testDB" auth="Container" type="javax.sql.DataSource"
factory="com.junducki.blog.dbcpEncryptDataSourceFacroty"
initialSize="5" maxActive="10" maxIdle="5" maxWait="15000"
username="Owm8SZ89IPJLdu2pR84DQg==" password="3N+gGSCi3+9N2v43K18e5A=="
driverClassName="oracle.jdbc.driver.OracleDriver"
url="8l1Z5t8i0tpJJaMA0hE5NjAfbfQL1UXxNEKWxyPOYXkuB1bMqmOGir5QP9G9S/wh"
validationQuery="SELECT 1 FROM DUAL"
removeAbandoned="false" removeAbandonedTimeout="120" logAbandoned="false" />
</Context>
위와 같이 factory 부분은 변경 된 DataSourceFactory가 적용되었고, url, username, password는 암호화 된 정보로 설정되어 있다.
위 변경 전후의 설정은 맞지가 않다. 이 Factory는 제가 운영하는 Tomcat에 적용되어 있기 때문에 암호화 방식과 키관리 룰은 공개를 할 수 없기 때문이다. 단, 위의 내용으로 조금만 수정하면 쉽게 적용할 수 있을 것이다.