例如調整前,密碼以明碼 primary.datasource.password=databasePassword設定,或加密後程式內自行解密
例如調整後,密碼以primary.datasource.password=${KEYSTORE:primary_datasource_password},系自動將其從KeyStore取得並替代內容值,無須個別程式處理加解密。
位置:src/main/java/tw/lewishome/webapp/GlobalConstants.java
package tw.lewishome.webapp;
import java.io.File;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.passay.CharacterRule;
import org.passay.EnglishCharacterData;
import org.passay.EnglishSequenceData;
import org.passay.IllegalSequenceRule;
import org.passay.LengthRule;
import org.passay.RepeatCharactersRule;
import org.passay.Rule;
import org.passay.WhitespaceRule;
import tw.lewishome.webapp.base.utility.common.Aes256Utils;
import tw.lewishome.webapp.base.utility.common.KeyStoreUtils;
import tw.lewishome.webapp.base.utility.common.PassayUtils;
import tw.lewishome.webapp.base.utility.common.PropUtils;
import tw.lewishome.webapp.base.utility.common.RSAUtils;
/**
* 系統基礎常數定義類別,提供專案中常用的路徑、環境變數、加密設定等靜態常數。
*
* 主要功能包含:
* 1. 定義專案套件名稱。
* 2. 定義外部檔案資料夾、上傳、下載、預設路徑。
* 3. 提供系統可用環境變數清單及其說明。
* 4. 系統啟動時產生的 UUID 及 JWT Secret Key。
* 5. 系統 Spring Boot Profile、主機名稱等資訊。
* 6. 預設加密相關設定(AES Salt、RSA Key、KeyStore)。
*
* 本類別所有成員皆為 static,方便全域存取。
*
* @author Lewis
* @version 1.0
*/
public class GlobalConstants {
/** Private constructor to prevent instantiation */
GlobalConstants() {
throw new UnsupportedOperationException("This is a utility class and cannot be instantiated.");
}
/**
* system File separator windows will be backslash Unix will be forward slash
*/
public static final String SEPARATOR = File.separator;
/** user home path */
public static final String USER_HOME = System.getProperty("user.home");
/** Constant <code>PackageName="tw.lewishome.webapp"</code> */
public static final String ROOT_PACKAGE_NAME = GlobalConstants.class.getClass().getPackage().getName();
/** Constant <code>DefaultCharSet="UTF-8"</code> */
public static final String DEFAULT_CHAR_SET = "UTF-8";
/** base external file folder */
public static final String EXTERNAL_FOLDER = USER_HOME + SEPARATOR + "webappData";
/** base file upload folder */
public static final String UPLOAD_PATH = GlobalConstants.EXTERNAL_FOLDER + SEPARATOR + "upload" + SEPARATOR;
/** base file download folder */
public static final String DOWNLOAD_PATH = GlobalConstants.EXTERNAL_FOLDER + SEPARATOR + "download" + SEPARATOR;
/** base file default folder */
public static final String DEFAULT_FOLDER_PATH = GlobalConstants.EXTERNAL_FOLDER + SEPARATOR + "default"
+ SEPARATOR;
/** Default RSAKey File */
public static final String RSA_KEYFILE = "RSAKey.properties";
/** String to hold the KeyStore file */
public static final String KEY_STORE_FILE = "KeyStore.jceks";
/** String to hold the default properties file */
public static final String PROPERTIES_FILE = "default.Properties";
/** 啟動時產生 randomUUID 設定為 SystemBootUuid 變數。 */
public static final String SYSTEM_BOOT_UUID = UUID.randomUUID().toString().replaceAll("\\-", "");
// 以下是系統開啟時,由 SystemApplicationListener 設定的參數
/** 系統Spring Boot active profile (ENV) */
public static String SYS_PROFILE_ACTIVE = "DEV";
/** Default KeyStorePassword */
public static String KEY_STORE_PASSWORD = getKeyStorePassword();
/** Default AES256Salt Property Key */
public static String AES256_SALT_KEY = "AES256_SALT_KEY";
/** Default AES256Salt IV Property Key */
public static String AES256_IV_KEY = "AES256_IV_KEY";
/** Default AES256Salt IV Property Key */
public static String AES256_PASSWORD_KEY = "AES256_PASSWORD_KEY";
/** 系統EndPoint清單 */
public static List<String> LIST_SYSTEM_ENDPOINT = new ArrayList<>();
/** 執行Server name (host name) */
public static String HOST_SERVER_NAME = "";
/**
* 系統可用的環境變數清單,無須每次使用時再讀取資料
* (OS_ENV_VAR/Spring_Properties/container_variables/default.Properties)
*/
public static Map<String, String> ENV_VAR = new HashMap<>();
/**
* 系統產出 JWT Token 時的,secret key string,設計每次開機變更
* 所以使用SYSTEM_BOOT_UUID取的RandomUUid,轉換為HexString
*/
public static String JWT_SECRET = Base64.getEncoder().encodeToString(SYSTEM_BOOT_UUID.toString().getBytes());
public static String getKeyStorePassword() {
String keyStorePassword = "";
String encryptkeyStorePassword = PropUtils.getProps("KEY_STORE_PASSWORD");
try {
if (StringUtils.isBlank(encryptkeyStorePassword)){
// 1. 產生 KeyStore Password
String ketStorePassword = generateKeyStorePassword();
// 2. PUBLIC_KEY 作為 AES256 的 Salt Key 寫入 KeyStore 中
Map<String, String> mapKeyParis = RSAUtils.loadKeyParisString();
String defaultSlat = mapKeyParis.get(RSAUtils.PUBLIC_KEY);
KeyStoreUtils.setAliasDataToDefaultKeyStore(ketStorePassword,"AES256_SALT_KEY", defaultSlat);
AES256_SALT_KEY = defaultSlat;
// 3. DEFAULT_IV_KEY 作為 AES256 的 IV Key 寫入 KeyStore 中
String defaultIvKey = Aes256Utils.getDefaultStringIv();
KeyStoreUtils.setAliasDataToDefaultKeyStore(ketStorePassword,"AES256_IV_KEY", defaultIvKey);
AES256_IV_KEY = defaultIvKey;
//
String aes256Password = generateKeyStorePassword();
KeyStoreUtils.setAliasDataToDefaultKeyStore(ketStorePassword,"AES256_PASSWORD_KEY", aes256Password);
AES256_PASSWORD_KEY = aes256Password;
encryptkeyStorePassword = RSAUtils.encryptStringByPrivateKey(ketStorePassword);
PropUtils.updateProperty("KEY_STORE_PASSWORD", encryptkeyStorePassword);
keyStorePassword = Aes256Utils.decryptAES256(encryptkeyStorePassword);
} else {
keyStorePassword = RSAUtils.decryptStringByPublicKey(encryptkeyStorePassword);
AES256_SALT_KEY = KeyStoreUtils.getAliasDataFromKeystore(keyStorePassword,"AES256_SALT_KEY");
AES256_IV_KEY = KeyStoreUtils.getAliasDataFromKeystore(keyStorePassword,"AES256_IV_KEY");
AES256_PASSWORD_KEY = KeyStoreUtils.getAliasDataFromKeystore(keyStorePassword,"AES256_PASSWORD_KEY");
}
} catch (Exception e) {
new RuntimeException("取得 KeyStore Password 發生錯誤", e);
}
return keyStorePassword;
}
/**
* 依 passwordRules() 來產生8個字元的密碼
*
* @return String Generated Password
*/
public static String generateKeyStorePassword() {
List<CharacterRule> listCharacterRule = new ArrayList<>();
List<Rule> rules = passwordKeyStoreRules();
for (Rule oneRule : rules) {
if (oneRule instanceof CharacterRule) {
listCharacterRule.add((CharacterRule) oneRule);
}
}
return PassayUtils.generatePassword(10 , listCharacterRule);
}
/**
* 密碼規則
* 1. 8-16 個字元
* 2. 至少一個英文大寫
* 3. 至少一個英文小寫
* 4. 至少一個數字
* 5. 至少一個符號
* 6. 不可以與 username 相同
* 7. 不可以有空白字元
* 8. 不可以連續英文 >= 5 如 'abcde' 參數 false 表示z-a 不算連續如 'xyzabc'`
* 9. 不可以連續數字 >= 5 如 '34567' 參數 false 表示z-a 不算連續如 '09123'
* 10. 不可以重複文數字 >= 4 如 '1111' 或 'aaaa'
*
* @return ArrayList Rule
*/
public static List<Rule> passwordKeyStoreRules() {
List<Rule> pwdRules = new ArrayList<Rule>();
// 1. length between 8 and 16 characters
pwdRules.add(new LengthRule(8, 16));
// 2. at least one upper-case character
pwdRules.add(new CharacterRule(EnglishCharacterData.UpperCase, 1));
// 3. at least one lower-case character
pwdRules.add(new CharacterRule(EnglishCharacterData.LowerCase, 1));
// 4. at least one digit character
pwdRules.add(new CharacterRule(EnglishCharacterData.Digit, 1));
// 5. no whitespace
pwdRules.add(new WhitespaceRule());
// define some illegal sequences that will fail when >= 5 chars long
// alphabetical is of the form 'abcde', numerical is '34567'
// the false parameter indicates that wrapped sequences are allowed; e.g.
// 'xyzabc'
// 8. define some illegal sequences that will fail when >= 5 chars long
pwdRules.add(new IllegalSequenceRule(EnglishSequenceData.Alphabetical, 5, false));
// 9. define some illegal sequences that will fail when >= 5 chars long
pwdRules.add(new IllegalSequenceRule(EnglishSequenceData.Numerical, 5, false));
// 10 can not repeat long >= 4 char
pwdRules.add(new RepeatCharactersRule(4));
// pwdRules.add(new AllowedCharacterRule(new char[] { 'a', 'b', 'c' }));
return pwdRules;
}
}
位置:src/main/java/tw/lewishome/webapp/PropertySourceKeyStoreProcessor.java
package tw.lewishome.webapp;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import tw.lewishome.webapp.base.utility.common.KeyStoreUtils;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* KeyStore 屬性來源後處理器
*
* <p>此類實現 Spring Boot 的 {@link EnvironmentPostProcessor} 介面,
* 用於在應用程式啟動時處理環境屬性。它掃描所有屬性來源,
* 尋找符合 <code>${KEYSTORE:alias}</code> 格式的佔位符,
* 並從 KeyStore 中解析對應的值。</p>
*
* <h2>功能說明</h2>
* <ul>
* <li>掃描所有可列舉的屬性來源</li>
* <li>識別包含 KEYSTORE 參考的屬性</li>
* <li>從 KeyStore 檔案中擷取加密的值</li>
* <li>建立新的 MapPropertySource 包含解密後的值,並將其插入到原始來源之前</li>
* <li>確保解密後的值優先於原始佔位符</li>
* </ul>
*
* <h2>執行順序</h2>
* <p>此處理器以 {@link Ordered#HIGHEST_PRECEDENCE} + 40 的優先級執行,
* 以確保 KeyStore 值在 ConfigurationPropertySourcesConverter 之前被解析。</p>
*
* <h2>用法範例</h2>
* <p>在屬性檔案中使用以下格式引用 KeyStore 中的值:</p>
* <pre>
* database.password=${KEYSTORE:db-password-alias}
* api.secret=${KEYSTORE:api-secret-alias}
* </pre>
*
* <h2>異常處理</h2>
* <p>若解析 KeyStore 值時發生異常,系統將輸出錯誤訊息,
* 並保持原始的佔位符字串以供後續處理。</p>
*/
public class PropertySourceKeyStoreProcessor implements EnvironmentPostProcessor, Ordered {
// 正則表達式模式,用於匹配 ${KEYSTORE:alias} 格式的佔位符
private static final Pattern KEYSTORE_PATTERN = Pattern.compile("\\$\\{KEYSTORE:([^}]+)\\}");
/**
* 後處理環境中的屬性源,解析並解密所有包含金鑰庫參考的屬性。
* <p>
* 此方法遍歷所有可列舉的屬性源,搜尋包含 "${KEYSTORE:" 前綴的屬性值。
* 對於找到的每個金鑰庫參考,使用 {@link #resolveKeystoreReference(String)}
* 方法進行解密,並將解密後的屬性添加到新的 {@link MapPropertySource} 中。
* </p>
* <p>
* 新的屬性源會被插入到原始屬性源之前,以確保解密後的值具有更高的優先級。
* </p>
*
* @param environment 可配置的應用環境,包含所有屬性源
* @param application Spring Boot 應用程式實例
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, org.springframework.boot.SpringApplication application) {
// 找尋所有屬性來源
for (PropertySource<?> source : environment.getPropertySources()) {
// 僅處理可列舉的屬性來源
if (source instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
Map<String, Object> decryptedProperties = new HashMap<>();
boolean hasKeystoreRef = false;
// 掃描所有屬性名稱
for (String propertyName : enumerable.getPropertyNames()) {
Object valueObj = enumerable.getProperty(propertyName);
if (valueObj instanceof String) {
String value = (String) valueObj;
// 檢查是否包含 KEYSTORE 參考
if (value.contains("${KEYSTORE:")) {
// 嘗試解析 KEYSTORE 參考
String decrypted = resolveKeystoreReference(value);
if (!decrypted.equals(value)) {
decryptedProperties.put(propertyName, decrypted);
hasKeystoreRef = true;
}
}
}
}
// 如果找到任何 KEYSTORE 參考,則建立新的屬性來源
if (hasKeystoreRef) {
MapPropertySource decryptedSource = new MapPropertySource(
source.getName() + "-keystore-resolved",
decryptedProperties);
// 將新的屬性來源插入到原始來源之前
environment.getPropertySources().addBefore(source.getName(), decryptedSource);
}
}
}
}
/**
* Resolve ${KEYSTORE:alias} references by fetching the value from KeyStore
*/
private String resolveKeystoreReference(String value) {
// 使用Matcher來尋找KEYSTORE是否有 KEYSTORE_PATTERN的格式
Matcher matcher = KEYSTORE_PATTERN.matcher(value);
if (!matcher.find()) {
return value;
}
StringBuffer resolved = new StringBuffer();
matcher.reset();
// 逐一解析所找到的的KEYSTORE參考
while (matcher.find()) {
String alias = matcher.group(1);
try {
// 從KeyStore中取得對應的值
String keystoreValue = KeyStoreUtils.getAliasDataFromKeystore(
GlobalConstants.KEY_STORE_FILE,
GlobalConstants.KEY_STORE_PASSWORD,
alias);
// 用解析後的值替換原始Value中的KEYSTORE參考
matcher.appendReplacement(resolved, Matcher.quoteReplacement(keystoreValue));
} catch (Exception e) {
System.err.println("Failed to resolve KEYSTORE:" + alias + " - " + e.getMessage());
// Keep original placeholder on error
matcher.appendReplacement(resolved, Matcher.quoteReplacement(matcher.group(0)));
}
}
// Append the remaining part of the original value
matcher.appendTail(resolved);
return resolved.toString();
}
/**
* 取得此屬性源後處理器的執行順序。
*
* <p>此方法傳回的值決定了在 Spring 環境中屬性源被處理的優先級順序。
* 返回值越小,執行優先級越高。
*
* @return 執行順序,設定為 {@link Ordered#HIGHEST_PRECEDENCE} + 40。
* 確保 KeyStore 的值在 ConfigurationPropertySourcesConverter 之前被解析。
*/
@Override
public int getOrder() {
// 設定執行順序,確保在 ConfigurationPropertySourcesConverter 之前執行
return Ordered.HIGHEST_PRECEDENCE + 40;
}
}
位置:src/main/java/tw/lewishome/webapp/PropertySourceKeyStoreProcessor.java
package tw.lewishome.webapp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* ConfigurationPropertySourcesConverter 是一個實現 {@link EnvironmentPostProcessor}
* 和 {@link Ordered} 的類別,
* 用於處理 Spring 應用程序的環境配置屬性源。
*
* <p>
* 此類別的主要功能是遍歷 {@link ConfigurableEnvironment} 中的屬性源,尋找
* {@link org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource}
* 類型的屬性源,
* 並從中提取配置屬性。提取的屬性將被包裝在 {@link MapPropertySource} 中,並插入到環境的屬性源列表中。
* </p>
*
* <p>
* 此類別的工作流程如下:
* </p>
* <ol>
* <li>在
* {@link #postProcessEnvironment(ConfigurableEnvironment, SpringApplication)}
* 方法中調用 {@link #process(ConfigurableEnvironment)} 方法。</li>
* <li>在 {@link #process(ConfigurableEnvironment)} 方法中,遍歷環境中的所有屬性源。</li>
* <li>檢查每個屬性源是否為
* {@link org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource}
* 類型。</li>
* <li>如果找到,則嘗試提取其配置屬性並將其添加為新的 {@link MapPropertySource}。</li>
* </ol>
*
* <p>
* 此類別的優先順序由 {@link #getOrder()} 方法定義,預設為 {@link Ordered#HIGHEST_PRECEDENCE} +
* 50。
* </p>
*
*/
public class PropertySourcesConverter implements EnvironmentPostProcessor, Ordered {
private static final Logger logger = LoggerFactory.getLogger(PropertySourcesConverter.class);
/**
* 處理應用程序的環境設置。
*
* 此方法在應用程序啟動過程中被調用,以便對可配置的環境進行後處理。
*
* @param environment 可配置的環境,包含應用程序的屬性和配置。
* @param application 當前的 Spring 應用程序實例。
*
* @throws Exception 如果處理環境時發生錯誤,將記錄調試信息。
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
try {
process(environment);
} catch (Exception e) {
logger.debug("Failed to process ConfigurationPropertySourcesPropertySource: {}", e.toString());
}
}
/**
* 處理給定的 ConfigurableEnvironment,從中提取配置屬性源並將其添加為 MapPropertySource。
*
* <p>
* 此方法遍歷環境中的所有 PropertySource,尋找特定類型的 PropertySource
* (即 ConfigurationPropertySourcesPropertySource)。對於每個找到的配置屬性源,
* 該方法嘗試提取其底層源並將其轉換為 Map。如果成功提取到的 Map 不為空,
* 則將其作為新的 MapPropertySource 插入到環境中,並在日誌中記錄相關信息。
* </p>
*
* @param environment 要處理的 ConfigurableEnvironment 實例
*/
private void process(ConfigurableEnvironment environment) {
// 找尋所有 PropertySource 來源
for (PropertySource<?> ps : environment.getPropertySources()) {
if (ps == null)
continue;
String className = ps.getClass().getName();
// 檢查是否為 ConfigurationPropertySourcesPropertySource 類型
if ("org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertySource"
.equals(className)) {
logger.info("Found ConfigurationPropertySourcesPropertySource: {}", ps.getName());
try {
// 嘗試取得配置屬性來源
Object cfgPropSources = tryGetConfigurationPropertySources(ps);
// 如果成功取得,則進行 Looping 處理
if (cfgPropSources != null) {
Iterator<?> it = asIterator(cfgPropSources);
int idx = 0;
while (it != null && it.hasNext()) {
Object cfgSource = it.next();
// 嘗試取得底層來源
Object underlying = tryGetUnderlyingSource(cfgSource);
if (underlying != null) {
// 嘗試從底層來源提取 Map
Map<String, Object> extracted = tryExtractMap(underlying);
if (extracted != null && !extracted.isEmpty()) {
// 建立並插入新的 MapPropertySource
String newName = ps.getName() + "-cfg-extracted-" + idx++;
MapPropertySource mps = new MapPropertySource(newName, extracted);
// Insert before the original source
environment.getPropertySources().addBefore(ps.getName(), mps);
logger.info("Inserted extracted MapPropertySource '{}' with {} entries", newName,
extracted.size());
}
}
}
}
} catch (Throwable t) {
logger.debug("Error while extracting configuration property sources: {}", t.toString());
}
}
}
}
/**
* 將給定的物件轉換為Iterator。
*
* @param iterable 要轉換的物件,可以是任何實現了 Iterable 接口的物件,
* 或者具有 iterator() 方法的物件。
* @return 如果物件為 null,則返回 null;如果物件是 Iterable 的實例,
* 則返回其迭代器;如果物件具有 iterator() 方法,則返回該方法的返回值,
* 否則返回 null。
*/
private Iterator<?> asIterator(Object iterable) {
if (iterable == null)
return null;
if (iterable instanceof Iterable) {
return ((Iterable<?>) iterable).iterator();
}
try {
Method iterator = iterable.getClass().getMethod("iterator");
Object it = iterator.invoke(iterable);
if (it instanceof Iterator)
return (Iterator<?>) it;
} catch (Exception ignored) {
}
return null;
}
/**
* 嘗試獲取配置屬性來源。
*
* 此方法首先檢查給定的 PropertySource 物件是否具有名為
* "getConfigurationPropertySources" 的方法。如果存在,則調用該方法並返回其結果。
* 如果該方法不存在,則嘗試訪問名為 "configurationPropertySources" 的私有字段。
* 如果該字段也不存在,則最後嘗試訪問名為 "sources" 的私有字段。
*
* @param ps 要檢查的 PropertySource 物件
* @return 返回配置屬性來源的對象,如果無法獲取,則返回 null
*/
private Object tryGetConfigurationPropertySources(PropertySource<?> ps) {
try {
Method m = ps.getClass().getMethod("getConfigurationPropertySources");
return m.invoke(ps);
} catch (Exception e) {
try {
Field f = ps.getClass().getDeclaredField("configurationPropertySources");
f.setAccessible(true);
return f.get(ps);
} catch (Exception ex) {
try {
Field f2 = ps.getClass().getDeclaredField("sources");
f2.setAccessible(true);
return f2.get(ps);
} catch (Exception ignored) {
}
}
}
return null;
}
/**
* 嘗試從給定的配置來源中獲取底層來源。
*
* @param cfgSource 要檢查的配置來源對象,可能為 null。
* @return 如果找到底層來源,則返回該來源;否則返回 null。
*
* 此方法將檢查配置來源對象的多個方法和字段,以獲取其底層來源。
* 它首先嘗試調用以下方法:getUnderlyingSource、getSource、getMap 和 getProperties。
* 如果這些方法都不存在或返回 null,則將檢查名為 source 和 underlyingSource 的字段。
*
* 如果在任何步驟中找到非 null 的值,則立即返回該值。
* 如果所有檢查都未找到有效的來源,則返回 null。
*/
private Object tryGetUnderlyingSource(Object cfgSource) {
if (cfgSource == null)
return null;
try {
for (String mName : new String[] { "getUnderlyingSource", "getSource", "getMap", "getProperties" }) {
try {
Method m = cfgSource.getClass().getMethod(mName);
Object val = m.invoke(cfgSource);
if (val != null)
return val;
} catch (NoSuchMethodException ignored) {
}
}
for (String fName : new String[] { "source", "underlyingSource" }) {
try {
Field f = cfgSource.getClass().getDeclaredField(fName);
f.setAccessible(true);
Object val = f.get(cfgSource);
if (val != null)
return val;
} catch (NoSuchFieldException ignored) {
}
}
} catch (Exception ignored) {
}
return null;
}
/**
* 嘗試從給定的物件中提取屬性映射。
*
* <p>
* 此方法接受一個物件,並根據其類型提取屬性名稱和值,返回一個包含這些屬性的映射。如果物件為 {@code null},則返回 {@code null}。
* </p>
*
* <p>
* 支持的物件類型包括:
* </p>
* <ul>
* <li>{@link Map}:直接將其內容複製到返回的映射中。</li>
* <li>{@link PropertySource}:如果是
* {@link org.springframework.core.env.EnumerablePropertySource},則提取其所有屬性名稱和值。</li>
* <li>其他類型:通過反射查找 {@code getPropertyNames} 和 {@code getProperty} 方法來提取屬性。</li>
* </ul>
*
* @param underlying 要提取屬性的物件
* @return 包含屬性名稱和值的映射,如果未能提取任何屬性則返回 {@code null}
*/
@SuppressWarnings("unchecked")
private Map<String, Object> tryExtractMap(Object underlying) {
// 如果底層來源為 null,則返回 null
if (underlying == null)
return null;
Map<String, Object> map = new HashMap<>();
try {
// 檢查底層來源的類型並提取屬性
if (underlying instanceof Map) {
// 處理 Map 類型
Map<String, Object> u = (Map<String, Object>) underlying;
map.putAll(u);
return map;
}
// 嘗試處理 PropertySource 類型
if (underlying instanceof PropertySource) {
PropertySource<?> ps = (PropertySource<?>) underlying;
if (ps instanceof org.springframework.core.env.EnumerablePropertySource) {
// 處理 EnumerablePropertySource 類型
org.springframework.core.env.EnumerablePropertySource<?> eps = (org.springframework.core.env.EnumerablePropertySource<?>) ps;
for (String name : eps.getPropertyNames()) {
map.put(name, eps.getProperty(name));
}
return map;
}
}
// 嘗試通過反射調用 getPropertyNames 和 getProperty 方法
try {
Method m = underlying.getClass().getMethod("getPropertyNames");
Object names = m.invoke(underlying);
if (names instanceof String[]) {
// 處理屬性名稱陣列
for (String n : (String[]) names) {
try {
Method gm = underlying.getClass().getMethod("getProperty", String.class);
Object v = gm.invoke(underlying, n);
map.put(n, v);
} catch (Exception ignored) {
}
}
return map;
}
} catch (Exception ignored) {
}
} catch (Exception ignored) {
ignored.printStackTrace();
}
// 如果無法提取任何屬性,則返回 null ,否則返回提取的映射
return map.isEmpty() ? null : map;
}
/**
* 取得此屬性來源的優先順序。
*
* <p>
* 回傳的優先順序值為 {@link Ordered#HIGHEST_PRECEDENCE} + 50,
* 表示此屬性來源在較高優先順序位置被載入。
* 數值越小,優先順序越高。
* </p>
*
* @return 優先順序值,為 {@link Ordered#HIGHEST_PRECEDENCE} + 50
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 50;
}
}
位置:src/main/resources/application-dev.propertiesprimary.datasource.password=${KEYSTORE:primary_datasource_password} 以及secondary.datasource.password=${KEYSTORE:secondary_datasource_password}
#各類資料庫的 url 參考:
# H2 database ==> jdbc:h2:mem:testdb
# SQL-Server ==> jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]]
# example ==> jdbc:sqlserver://sql.lewis-home.tw:1433;databasename=MBS;encrypt=false
# MySQL ==> jdbc:mysql://[hosts][:portNumber][/database]
# example ==> jdbc:mysql://mysql.lewis-home.tw:33060
# AS400(Jt400)==> jdbc:as400://[hosts][;property=value]
# example ==> jdbc:as400://as400system;transaction isolation=none;translate binary=true;date format=iso;prompt=false
# Oracle ==> jdbc:oracle:thin:@[HOST][:PORT]:SID or jdbc:oracle:thin:@//[HOST][:PORT]/SERVICE
# example ==> jdbc:oracle:thin:@oracle.lewis-home.tw:1521:oracle.lewis-home.tw
# postgres ==> jdbc:postgresql://@[netloc][:port][/dbname][?param1=value1&...]
# example ==> jdbc:postgresql://postsql.lewis-home.tw:5432/database
#各類資料庫的 driver class Name
# H2 database ==> org.h2.Driver
# SQL-Server ==> com.microsoft.sqlserver.jdbc.SQLServerDriver
# MySQL ==> com.mysql.jdbc.Driver
# AS400(Jt400)==> com.ibm.as400.access.AS400JDBCDriver
# Oracle ==> oracle.jdbc.driver.OracleDriver
# postgres ==> org.postgresql.Driver
#Store primary Datasource (這些是自訂的變數名稱,只要與程式內取用的設定一致即可)
primary.datasource.enabled=true
primary.datasource.jdbcurl=jdbc:sqlserver://sql.lewishome.tw:1433;databasename=DbMuserXX1;encrypt=false;characterEncoding=utf-8
primary.datasource.username=MuserXX1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
primary.datasource.password=${KEYSTORE:primary_datasource_password}
primary.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver
primary.datasource.hibernate.hbm2ddl.auto=update
#Secondary Datasource 是否有需要
secondary.datasource.enabled=true
secondary.datasource.jdbcurl=jdbc:oracle:thin:@oracle.lewishome.tw:1521/orclpdb.lewishome.tw
secondary.datasource.username=OUSERXX1
# Database connection password 建議不要存在此駔,使用 Keystore(安全線以及後續密碼交出去給資管理部)
secondary.datasource.password=${KEYSTORE:secondary_datasource_password}
secondary.datasource.driverClassName=oracle.jdbc.OracleDriver
secondary.datasource.hibernate.hbm2ddl.auto=update
**(這個還在努力改成 Java Config中)**
位置:src/main/resources/META_INF/spring.factories
org.springframework.boot.env.EnvironmentPostProcessor=\
tw.lewishome.webapp.PropertySourceKeyStoreProcessor,\
tw.lewishome.webapp.PropertySourcesConverter
位置:src/test/java/tw/lewishome/webapp/EncryptPasswordTool.java
package tw.lewishome.webapp;
import org.junit.jupiter.api.Test;
import tw.lewishome.webapp.base.utility.common.KeyStoreUtils;
import tw.lewishome.webapp.base.utility.common.PropUtils;
import tw.lewishome.webapp.base.utility.common.RSAUtils;
/**
* Utility tool to encrypt database password and store in KeyStore
* Usage: Run this test/tool to encrypt "XXXXX" (實際密碼) and store in KeyStore
*/
public class EncryptPasswordTool {
@Test
public void encryptPropertiesPassword() throws Exception {
String keyStorePath = "KeyStore.jceks";
String encryptKeyStorePassword = PropUtils.getProps("KEY_STORE_PASSWORD");
String keyStorePassword = RSAUtils.decryptStringByPublicKey(encryptKeyStorePassword);
String alias = "primary_datasource_password";
String plainPassword = "XXXXXXXX';//換為實際密碼
KeyStoreUtils.setAliasDataToKeyStore(keyStorePath, keyStorePassword, alias, plainPassword);
// Encrypt and store the password in KeyStore
KeyStoreUtils.setAliasDataToKeyStore(keyStorePath, keyStorePassword, alias, plainPassword);
System.out.println("Password encrypted and stored successfully!");
System.out.println("Alias: " + alias);
System.out.println("primary.datasource.password=${KEYSTORE:" + alias + "}");
// Also encrypt secondary datasource password
String secondaryAlias = "secondary_datasource_password";
String secondaryPassword = "XXXXXXXX"; //換為實際密碼
KeyStoreUtils.setAliasDataToKeyStore(keyStorePath, keyStorePassword, secondaryAlias, secondaryPassword);
System.out.println("\nSecondary password encrypted and stored successfully!");
System.out.println("Alias: " + secondaryAlias);
System.out.println("secondary.datasource.password=${KEYSTORE:" + secondaryAlias + "}");
System.out.println("\nKeyStore File: " + keyStorePath);
// Verify by retrieving the passwords
String retrievedPassword = KeyStoreUtils.getAliasDataFromKeystore(keyStorePath, keyStorePassword, alias);
System.out.println("\nVerification - Primary password: " + retrievedPassword);
String retrievedSecondaryPassword = KeyStoreUtils.getAliasDataFromKeystore(keyStorePath, keyStorePassword,
secondaryAlias);
System.out.println("Verification - Secondary password: " + retrievedSecondaryPassword);
try {
String keystoreValue = KeyStoreUtils.getAliasDataFromKeystore(
GlobalConstants.KEY_STORE_FILE,
GlobalConstants.KEY_STORE_PASSWORD,
alias);
System.out.println("Resolved KEYSTORE:" + alias + " = " + keystoreValue);
} catch (Exception e) {
System.err.println("Failed to resolve KEYSTORE:" + alias + " - " + e.getMessage());
// Keep original placeholder on error
}
}
}