在傳統 C/S(Client/Server)架構的 Java 桌面應用中,客戶端直連數據庫,如何妥善保護數據庫連接配置(尤其是用戶名/密碼)是關鍵安全課題。本文介紹一種將 MyBatis XML 配置文件加密存儲、運行時解密加載的方案,並展示單例模式管理 SqlSessionFactory 的最佳實踐。
mybatis-config.xml,包含數據源、映射器等配置。basic.data),放入 src 目錄隨 JAR 發布。InputStream → 傳給 SqlSessionFactoryBuilder.build()。<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="app_user"/>
<property name="password" value="plain_password"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="com/example/dao/UserMapper.xml"/>
</mappers>
</configuration>
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class DesUtil {
private static final String ALGORITHM = "DES";
private static final String KEY = "MyBatisKey"; // 8 字節密鑰,生產環境應從安全存儲讀取
public static String encrypt(String data) throws Exception {
SecureRandom sr = new SecureRandom();
DESKeySpec dks = new DESKeySpec(KEY.getBytes());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
SecretKey secretKey = keyFactory.generateSecret(dks);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, sr);
byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encrypted);
}
public static String decrypt(String encryptedData) throws Exception {
SecureRandom sr = new SecureRandom();
DESKeySpec dks = new DESKeySpec(KEY.getBytes());
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(ALGORITHM);
SecretKey secretKey = keyFactory.generateSecret(dks);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, sr);
byte[] decoded = Base64.getDecoder().decode(encryptedData);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, "UTF-8");
}
}
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.ByteArrayInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
public class MyBatisManager {
private static final String ENCRYPTED_CONFIG = "basic.data"; // 放在 classpath 根目錄
private static final MyBatisManager INSTANCE = new MyBatisManager();
private final SqlSessionFactory sqlSessionFactory;
private MyBatisManager() {
this.sqlSessionFactory = buildSessionFactory();
}
public static MyBatisManager getInstance() {
return INSTANCE;
}
public SqlSessionFactory getSqlSessionFactory() {
return sqlSessionFactory;
}
private SqlSessionFactory buildSessionFactory() {
InputStream inputStream = null;
try {
inputStream = loadDecryptedConfigStream();
if (inputStream == null) {
throw new IllegalStateException("無法加載解密後的 MyBatis 配置");
}
return new SqlSessionFactoryBuilder().build(inputStream);
} catch (Exception e) {
throw new ExceptionInInitializerError("MyBatis 初始化失敗: " + e.getMessage());
} finally {
if (inputStream != null) {
try { inputStream.close(); } catch (Exception ignored) {}
}
}
}
private InputStream loadDecryptedConfigStream() throws Exception {
// 獲取 classpath 下加密文件的絕對路徑
String classPath = MyBatisManager.class.getClassLoader().getResource("").getFile();
String decodedPath = java.net.URLDecoder.decode(classPath, "UTF-8");
File encryptedFile = new File(decodedPath + ENCRYPTED_CONFIG);
if (!encryptedFile.exists()) {
throw new IllegalStateException("加密配置文件不存在: " + encryptedFile.getAbsolutePath());
}
// 讀取加密內容
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new FileReader(encryptedFile))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
// 解密並轉為 InputStream
String decryptedXml = DesUtil.decrypt(sb.toString());
return new ByteArrayInputStream(decryptedXml.getBytes("UTF-8"));
}
}
public class UserDao {
public User findById(Long id) {
try (SqlSession session = MyBatisManager.getInstance().getSqlSessionFactory().openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectById(id);
}
}
}
| 技術點 | 說明 |
|---|---|
| 單例模式 | SqlSessionFactory 線程安全、重量級,全應用僅需一個實例,雙重檢查鎖或靜態內部類均可,本文用饿漢式簡化。 |
| 類加載器獲取路徑 | ClassLoader.getResource("") 獲取 classpath 根路徑,配合 URLDecoder.decode 處理空格/中文路徑。 |
| 流式解密 | 將解密後的字串轉為 ByteArrayInputStream,避免寫臨時明文文件到磁盤。 |
| 異常快速失敗 | 構造器中捕獲異常拋 ExceptionInInitializerError,確保單例初始化失敗時 JVM 能正確報錯。 |
SqlSessionFactory 及類加載器隔離。若完全不想用 XML,可用 org.apache.ibatis.session.Configuration 程式化構建:
DataSource dataSource = new PooledDataSource(driver, url, user, pwd);
Environment env = new Environment("dev", new JdbcTransactionFactory(), dataSource);
Configuration config = new Configuration(env);
config.addMapper(UserMapper.class); // 需映射器接口在 classpath
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(config);
此方式適合配置動態生成、多租戶動態切換數據源等場景,但映射器註解或 XML 仍需編譯期確定。
通過「配置文件加密存儲 + 運行時解密流式加載 + 單例 SqlSessionFactory」組合,可在 C/S 架構中有效保護數據庫憑證,同時保持 MyBatis 原生開發體驗。核心在於不落地明文、單例線程安全、異常快速失敗三原則。生產環境務必升級加密算法與密鑰管理體系,將安全風險降至最低。