場景痛點
- CS 客戶端部署在用戶機器,配置文件易被竄改、密碼洩露
- BS/CS 共用 Mapper XML,需統一管理
- 多環境(開發/測試/生產)配置隔離
- 單機版無應用服務器,需輕量級初始化
核心方案:加密配置文件 + 解密流構建 SqlSessionFactory
1. 原始 MyBatis 配置
<?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.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/app_db"/>
<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>
2. 雙向加密(AES/DES)生成 .data 文件
public class ConfigEncryptor {
public static void main(String[] args) throws Exception {
String xml = Files.readString(Paths.get("src/main/resources/mybatis-config.xml"));
String encrypted = AESUtil.encrypt(xml, "strong-secret-key-16bytes!!");
Files.write(Paths.get("src/main/resources/config.data"), encrypted.getBytes(StandardCharsets.UTF_8));
System.out.println("加密完成: config.data");
}
}
// AES 工具類(簡化版,生產用 GCM 模式)
public class AESUtil {
public static String encrypt(String data, String key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
public static String decrypt(String encrypted, String key) throws Exception {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "AES");
cipher.init(Cipher.DECRYPT_MODE, keySpec);
return new String(cipher.doFinal(Base64.getDecoder().decode(encrypted)), StandardCharsets.UTF_8);
}
}
3. MyBatis 初始化器(單例、解密流注入)
public final class MyBatisInitializer {
private static final String ENCRYPTED_CONFIG = "config.data";
private static final String DECRYPT_KEY = "strong-secret-key-16bytes!!"; // 生產從環境讀取
private static volatile SqlSessionFactory sqlSessionFactory;
private MyBatisInitializer() {}
public static SqlSessionFactory getSqlSessionFactory() {
if (sqlSessionFactory == null) {
synchronized (MyBatisInitializer.class) {
if (sqlSessionFactory == null) {
sqlSessionFactory = buildFactory();
}
}
}
return sqlSessionFactory;
}
private static SqlSessionFactory buildFactory() {
try (InputStream decryptedStream = getDecryptedConfigStream()) {
return new SqlSessionFactoryBuilder().build(decryptedStream);
} catch (IOException e) {
throw new ExceptionInInitializerError("MyBatis 初始化失敗: " + e.getMessage());
}
}
private static InputStream getDecryptedConfigStream() throws IOException {
// 1. 定位加密文件(支援 classpath、文件系統、JAR 內)
URL resource = MyBatisInitializer.class.getClassLoader().getResource(ENCRYPTED_CONFIG);
if (resource == null) {
// 嘗試文件系統(開發/打包後路徑差異)
File file = new File(System.getProperty("user.dir"), "config/" + ENCRYPTED_CONFIG);
if (file.exists()) {
return decryptFile(file);
}
throw new FileNotFoundException("找不到加密配置: " + ENCRYPTED_CONFIG);
}
try (InputStream encryptedIn = resource.openStream()) {
String encrypted = new String(encryptedIn.readAllBytes(), StandardCharsets.UTF_8);
String decrypted = AESUtil.decrypt(encrypted, DECRYPT_KEY);
return new ByteArrayInputStream(decrypted.getBytes(StandardCharsets.UTF_8));
}
}
private static InputStream decryptFile(File file) throws IOException {
String encrypted = Files.readString(file.toPath(), StandardCharsets.UTF_8);
String decrypted = AESUtil.decrypt(encrypted, DECRYPT_KEY);
return new ByteArrayInputStream(decrypted.getBytes(StandardCharsets.UTF_8));
}
// 關閉工廠(應用退出時調用)
public static void shutdown() {
if (sqlSessionFactory != null) {
// MyBatis SqlSessionFactory 無顯式 close,但可釋放連接池
// 若用 PooledDataSource,可反射獲取並關閉
sqlSessionFactory = null;
}
}
}
4. 業務代碼使用
public class UserService {
private final SqlSessionFactory sqlSessionFactory = MyBatisInitializer.getSqlSessionFactory();
public User findById(Long id) {
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
return mapper.selectById(id);
}
}
public void insert(User user) {
try (SqlSession session = sqlSessionFactory.openSession(true)) { // 自動提交
UserMapper mapper = session.getMapper(UserMapper.class);
mapper.insert(user);
}
}
}
進階:純 Java API 配置(無 XML)
public class JavaApiConfig {
public static SqlSessionFactory build() {
// 1. 數據源
PooledDataSource dataSource = new PooledDataSource();
dataSource.setDriver("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/app_db");
dataSource.setUsername("user");
dataSource.setPassword("pwd");
// 2. 環境
Environment env = new Environment("development",
new JdbcTransactionFactory(), dataSource);
// 3. 配置
Configuration config = new Configuration(env);
config.addMapper(UserMapper.class); // 註冊 Mapper 接口
config.setMapUnderscoreToCamelCase(true); // 下劃線轉駝峰
// 4. 構建
return new SqlSessionFactoryBuilder().build(config);
}
}
多環境隔離策略
| 策略 | 實現方式 | 優點 | 缺點 |
| 多配置文件 | mybatis-dev.xml, mybatis-prod.xml + 系統屬性切換 | 清晰分離 | 需維護多份 |
| 單文件多 environment | <environments default="${env}"> 多個 <environment id="dev"> | 單文件管理 | 密碼仍明文 |
| 外部屬性文件佔位符 | ${db.url} 從 properties 讀取,配合加密 | 敏感信息外部化 | 需額外解析層 |
| 加密文件 + 環境變量密鑰 | 本文方案:config.data + DECRYPT_KEY 從環境讀取 | 安全、單文件、多環境共用邏輯 | 需密鑰管理 |
安全加固建議
- 密鑰不入代碼:JVM 參數
-Dapp.crypto.key=...、環境變量、密鑰管理服務
- 升級 AES-GCM:提供完整性驗證,防篡改
- 簽名驗證:配置文件簽名,啟動時驗證防替換
- 最小權限原則:數據庫用戶僅擁有必要表的增刪改查權限
- 連接池監控:定期檢查連接洩漏、異常佔用
總結
CS 架構下 MyBatis 部署的核心難點是**配置安全與輕量初始化**。本文方案通過對稱加密配置文件 + 運行時解密流構建 SqlSessionFactory,實現了密碼不落地明文、單文件多環境、BS/CS 共用 Mapper。生產環境建議進一步引入密鑰管理服務、配置簽名驗證、連接池監控,構建纵深防禦體系。
- 上一篇文章: Java 深拷貝完整方案:序列化 vs JSON 兩種實現與效能對比
- 下一篇文章: Java 獲取時間方法總結:DateFormat、Timestamp、LocalDateTime 三種方案對比
Copyright ©
程式碼之家 All Rights Reserved