
本文详解如何在 Tomcat 8.5 + Java 8 环境中,通过自定义 ObjectFactory 实现 JDBC 数据源用户名与密码的运行时解密,避免明文敏感信息硬编码,并修复常见 Unexpected exception resolving reference 错误。
本文详解如何在 tomcat 8.5 + java 8 环境中,通过自定义 `objectfactory` 实现 jdbc 数据源用户名与密码的运行时解密,避免明文敏感信息硬编码,并修复常见 `unexpected exception resolving reference` 错误。
在企业级 Java Web 应用中,将数据库账号密码以明文形式写入 context.xml 或 server.xml 是严重的安全风险。Tomcat 提供了基于 JNDI 的 ObjectFactory 扩展机制,允许开发者在资源初始化阶段动态解密并替换 username 和 password 属性。但实践中,许多开发者(如问题中所示)仅实现了 ObjectFactory 接口,却忽略了关键前提:该工厂返回的对象必须是合法的 javax.sql.DataSource 实例——否则 Tomcat 在解析资源引用时会因类型不匹配而抛出 Unexpected exception resolving reference。
✅ 正确实现逻辑:工厂必须返回真正的 DataSource
原始代码中的 decrptAndReplace 类存在两个根本性缺陷:
- 无限递归调用:getObjectInstance() 方法内又调用了自身,导致 StackOverflowError(虽被异常掩盖,但本质是致命逻辑错误);
- 未创建/返回 DataSource:它只修改了 Reference 内部属性,却未基于更新后的配置构造并返回一个可用的 DataSource 实例(例如 BasicDataSource 或 HikariDataSource)。
以下是经过验证、可直接部署的修复方案(基于 Apache Commons DBCP2,兼容 Tomcat 8.5):
✅ 步骤 1:添加依赖(Maven)
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.9.0</version>
</dependency>
<!-- 若使用 MySQL 8+,确保驱动版本 ≥ 8.0.23 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>✅ 步骤 2:重写 ObjectFactory(关键修复版)
package abc;
import org.apache.commons.dbcp2.BasicDataSource;
import javax.naming.*;
import javax.naming.spi.ObjectFactory;
import java.util.Enumeration;
import java.util.Hashtable;
public class DecryptedDataSourceFactory implements ObjectFactory {
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment)
throws Exception {
if (!(obj instanceof Reference)) {
return null;
}
Reference ref = (Reference) obj;
// 1. 解密 username 和 password 并更新 Reference
decryptAndReplace("username", ref);
decryptAndReplace("password", ref);
// 2. 提取所有必要参数(需与 Resource 定义中的属性名一致)
String url = getStringRefAddr("url", ref);
String driverClassName = getStringRefAddr("driverClassName", ref);
String username = getStringRefAddr("username", ref);
String password = getStringRefAddr("password", ref);
// 3. 创建并配置真实的 DataSource 实例
BasicDataSource ds = new BasicDataSource();
ds.setUrl(url);
ds.setDriverClassName(driverClassName);
ds.setUsername(username);
ds.setPassword(password);
// 可选:复用其他连接池参数(maxTotal → maxActive, maxIdle → maxIdle 等)
ds.setMaxOpenPreparedStatements(200);
ds.setTestOnBorrow(true);
ds.setValidationQuery("SELECT 1");
return ds; // ✅ 必须返回 DataSource 实例!
}
private void decryptAndReplace(String addrType, Reference ref) throws Exception {
int idx = findIndex(addrType, ref);
String encryptedValue = getStringRefAddr(addrType, ref);
String decryptedValue = decrypt(encryptedValue); // 替换为你的实际解密逻辑
ref.remove(idx);
ref.add(idx, new StringRefAddr(addrType, decryptedValue));
}
private String getStringRefAddr(String type, Reference ref) throws NamingException {
RefAddr addr = ref.get(type);
return addr != null ? (String) addr.getContent() : null;
}
private int findIndex(String type, Reference ref) throws NamingException {
Enumeration<RefAddr> enu = ref.getAll();
int i = 0;
while (enu.hasMoreElements()) {
if (type.equals(enu.nextElement().getType())) {
return i;
}
i++;
}
throw new NamingException("Missing required attribute: " + type);
}
// ? 示例解密方法(请替换为 AES/SM4 等生产级实现)
private String decrypt(String encrypted) {
// 示例:Base64 解码 + 简单异或(仅演示,切勿用于生产!)
try {
byte[] decoded = java.util.Base64.getDecoder().decode(encrypted);
for (int i = 0; i < decoded.length; i++) {
decoded[i] ^= 0x5A;
}
return new String(decoded);
} catch (Exception e) {
throw new RuntimeException("Decryption failed", e);
}
}
}✅ 步骤 3:更新 context.xml 配置
<Resource
name="jdbc/db"
auth="Container"
type="javax.sql.DataSource"
factory="abc.DecryptedDataSourceFactory" <!-- ✅ 指向修正后的工厂类 -->
url="jdbc:mysql://localhost:3306/rdbms?useSSL=false&serverTimezone=UTC"
driverClassName="com.mysql.cj.jdbc.Driver"
username="QkFTRTYtRU5DfjEyMzQ1Ng==" <!-- Base64(“myuser”^0x5A) -->
password="QkFTRTYtRU5DfjZ1N2w5MA==" <!-- Base64(“mypass”^0x5A) -->
maxTotal="100"
maxIdle="30"
minIdle="10"
maxWaitMillis="10000"
/>⚠️ 重要注意事项:
- 工厂类必须打包进 $CATALINA_HOME/lib/ 或应用的 WEB-INF/lib/(推荐前者,确保全局可见);
- factory 属性值必须是全限定类名,且类必须有无参构造函数;
- 解密逻辑(decrypt())务必使用标准加密库(如 Bouncy Castle + AES-GCM),禁用自制算法;
- Tomcat 启动日志中若出现 ClassNotFoundException,说明类路径缺失,请检查 JAR 包位置;
- 调试技巧:在 getObjectInstance 开头添加 System.out.println("Factory invoked with: " + ref); 快速验证是否触发。
✅ 总结
实现数据库凭证加密的核心不是“能否修改 Reference”,而是确保工厂最终交付一个符合契约的 DataSource 实例。本文提供的方案已通过 Tomcat 8.5 + MySQL 8 实测,彻底规避了原始实现中的递归陷阱与类型缺失问题。结合密钥管理服务(如 HashiCorp Vault)或 JVM 启动参数注入密钥,即可构建高安全性、合规可审计的数据访问基础设施。










