iT邦幫忙

0

VScode 開發應用系統專案(4-3) - Spring Boot資料庫存取密碼保護

  • 分享至 

  • xImage
  •  

概述

  • Spring Boot資料庫設計與存取設計中,系統難免會使用密碼等資訊安全敏感的參數,例如資料連線的參數。關於這一類的參數,Java 本身由有提供 KeyStore的保存資料方式,於應用程式僅以ALias存取其內容,這理依Spring boot架構,設計將所有以${KEYSTORE:XXXXX}環境變數內容,皆由 KeyStore取得其內容。並保留以後新增資訊安全敏感的參數的架構。

例如調整前,密碼以明碼 primary.datasource.password=databasePassword設定,或加密後程式內自行解密
例如調整後,密碼以primary.datasource.password=${KEYSTORE:primary_datasource_password},系自動將其從KeyStore取得並替代內容值,無須個別程式處理加解密。

  • 因為KEYSTORE運作,本身就需要一個 KeyStorePassword (設定於GlobalConstants.KEY_STORE_PASSWORD),這是存取Keystore資料的重要資料,所以也不能用明碼紀錄,但因為沒有KeyStorePassword又不能從取KeyStore資料,所以設計將其以RAS PrivateKey加密,存於系統預設(或指定)的 Property檔案,系統啟動準備GlobalConstants時,再來讀取該Property檔案並解密使用。

準備與檢核

  1. 建置Spring Boot專案後系統自動產生了 application.properties。
  1. 工具類程式已經準備好可以使用。
  1. Spring boot 多資料庫支援的配置。

主要程式工作說明,已於Java Doc方式說明,請參考。

一、調整 GlobalConstants 系統基礎常數定義

位置: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;
    }
}

二、新增 PropertySourceKeyStoreProcessor 處理KeyStore

位置: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;
    }
}

三、新增 PropertySourcesConverter 處理置換變數

位置: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;
    }
}

四、調整 application-dev.properties DB密碼設定

位置:src/main/resources/application-dev.properties
primary.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

五、 META-INF 需要新增設定

**(這個還在努力改成 Java Config中)**

位置:src/main/resources/META_INF/spring.factories

org.springframework.boot.env.EnvironmentPostProcessor=\
  tw.lewishome.webapp.PropertySourceKeyStoreProcessor,\
  tw.lewishome.webapp.PropertySourcesConverter

六、新增EncryptPasswordTool 資料庫密碼存入KeyStore。

位置: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            
        }

    }
}


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言