src/main/java/com/webmanage/config/EncryptedPropertyDetector.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/java/com/webmanage/config/EncryptedPropertyEnvironmentPostProcessor.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/java/com/webmanage/util/AESUtil.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/resources/META-INF/spring.factories | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/resources/application-dev.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
src/main/resources/application-prod.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/webmanage/config/EncryptedPropertyDetector.java
New file @@ -0,0 +1,24 @@ package com.webmanage.config; import org.springframework.util.StringUtils; /** * 检测配置值是否为加密格式的工具 */ public class EncryptedPropertyDetector { private static final String PREFIX = "AES:"; public static boolean isEncrypted(String value) { return StringUtils.hasText(value) && value.startsWith(PREFIX); } public static String stripPrefix(String value) { if (!isEncrypted(value)) { return value; } return value.substring(PREFIX.length()); } } src/main/java/com/webmanage/config/EncryptedPropertyEnvironmentPostProcessor.java
New file @@ -0,0 +1,55 @@ package com.webmanage.config; import com.webmanage.util.AESUtil; 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.EnumerablePropertySource; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.util.StringUtils; import java.util.HashMap; import java.util.Map; /** * 在应用启动早期对敏感配置进行解密 */ public class EncryptedPropertyEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { Map<String, Object> overrides = new HashMap<>(); // 扫描所有可枚举属性源,收集以 AES: 开头的属性值并解密,放入最高优先级覆盖源 for (org.springframework.core.env.PropertySource<?> ps : environment.getPropertySources()) { if (ps instanceof EnumerablePropertySource) { EnumerablePropertySource<?> eps = (EnumerablePropertySource<?>) ps; for (String name : eps.getPropertyNames()) { Object raw = eps.getProperty(name); if (raw instanceof String) { String value = (String) raw; if (EncryptedPropertyDetector.isEncrypted(value)) { String cipher = EncryptedPropertyDetector.stripPrefix(value); String plain = AESUtil.decryptWithDefaultKey(cipher); overrides.put(name, plain); } } } } } if (!overrides.isEmpty()) { MutablePropertySources sources = environment.getPropertySources(); sources.addFirst(new MapPropertySource("decryptedSensitiveProperties", overrides)); } } // 确保尽早执行 @Override public int getOrder() { // 在配置文件加载完成后执行,避免拿不到值 return Ordered.LOWEST_PRECEDENCE; } } src/main/java/com/webmanage/util/AESUtil.java
New file @@ -0,0 +1,136 @@ package com.webmanage.util; import com.webmanage.common.BusinessException; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import javax.crypto.Cipher; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; /** * AES加密工具类 * * @author August * @date 2022-07-29 */ @Slf4j public class AESUtil { private AESUtil() { } /** * AES_KEY */ private static final String AES_KEY = "sD6i0YYW1a5FnzF6vRf13VtHocZabRcI"; /** * GCM_IV */ private static final String GCM_IV = "1SzB4YR0c0E9"; /** * GCM_TAG长度 */ private static final int GCM_TAG_LENGTH = 16; /** * 加密 * * @param plaintext 待加密文本 * @param key AES_KEY * @param iv GCM_IV * @return 密文 */ public static String encrypt(String plaintext, byte[] key, byte[] iv) { if (StringUtils.isBlank(plaintext) || key == null || iv == null) { throw new BusinessException("加密参数不能为空"); } try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec); // Perform Encryption byte[] cipherText = cipher.doFinal(plaintext.getBytes()); StringBuilder sb = new StringBuilder(); for (byte b : cipherText) { String hex = Integer.toHexString(b & 0xFF); if (hex.length() == 1) { hex = '0' + hex; } sb.append(hex); } return sb.toString(); } catch (Exception e) { throw new BusinessException("加密失败"); } } /** * 解密 * * @param cipherText 密文 * @param key AES_KEY * @param iv GCM_IV * @return 解密结果 */ public static String decrypt(String cipherText, byte[] key, byte[] iv) { if (StringUtils.isBlank(cipherText) || key == null || iv == null) { throw new BusinessException("解密参数不能为空"); } try { byte[] cipherTextByte = new byte[cipherText.length() / 2]; for (int i = 0; i < cipherText.length() / 2; i++) { int high = Integer.parseInt(cipherText.substring(i * 2, i * 2 + 1), 16); int low = Integer.parseInt( cipherText.substring(i * 2 + 1, i * 2 + 2), 16); cipherTextByte[i] = (byte) (high * 16 + low); } Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec keySpec = new SecretKeySpec(key, "AES"); GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv); cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmParameterSpec); byte[] decryptedText = cipher.doFinal(cipherTextByte); return new String(decryptedText); } catch (Exception e) { throw new BusinessException("解密失败"); } } /** * 使用默认密钥和IV进行解密 * @param cipherText 以十六进制字符串表示的密文 * @return 明文 */ public static String decryptWithDefaultKey(String cipherText) { return decrypt(cipherText, AES_KEY.getBytes(), GCM_IV.getBytes()); } public static void main(String[] args) { // String plainText = "zynlpt2024"; String plainText = "postgres"; log.info("Original Text : " + plainText); String cipherText = encrypt(plainText, AES_KEY.getBytes(), GCM_IV.getBytes()); log.info("Encrypted Text : " + cipherText); String decryptedText = decrypt(cipherText, AES_KEY.getBytes(), GCM_IV.getBytes()); log.info("DeCrypted Text : " + decryptedText); } } src/main/resources/META-INF/spring.factories
New file @@ -0,0 +1,2 @@ org.springframework.boot.env.EnvironmentPostProcessor=\ com.webmanage.config.EncryptedPropertyEnvironmentPostProcessor src/main/resources/META-INF/spring/org.springframework.boot.env.EnvironmentPostProcessor
New file @@ -0,0 +1,3 @@ com.webmanage.config.EncryptedPropertyEnvironmentPostProcessor src/main/resources/application-dev.yml
@@ -5,7 +5,7 @@ driver-class-name: org.postgresql.Driver url: jdbc:postgresql://localhost:5432/web_manage?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: postgres password: zkyxpostgres password: AES:d9d2d3e0d586e76d02a97c451f3256bffdc806b4c7626904 druid: # 初始连接数 initial-size: 5 src/main/resources/application-prod.yml
New file @@ -0,0 +1,70 @@ # 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: org.postgresql.Driver url: jdbc:postgresql://192.168.20.52:5432/zypt-v2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 username: postgres password: AES:f9d2d3e0d586e76d14aba9d24666acef98c4605be9ec680ac4cbeede7ad2 druid: # 初始连接数 initial-size: 5 # 最小连接池数量 min-idle: 10 # 最大连接池数量 max-active: 20 # 配置获取连接等待超时的时间 max-wait: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 time-between-eviction-runs-millis: 60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最大生存的时间,单位是毫秒 max-evictable-idle-time-millis: 900000 # 配置检测连接是否有效 validation-query: SELECT 1 test-while-idle: true test-on-borrow: false test-on-return: false # 打开PSCache,并且指定每个连接上PSCache的大小 pool-prepared-statements: true max-pool-prepared-statement-per-connection-size: 20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 filters: stat,wall,slf4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 配置DruidStatFilter web-stat-filter: enabled: true url-pattern: /* exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*" # 配置DruidStatViewServlet stat-view-servlet: enabled: true url-pattern: /druid/* reset-enable: false login-username: admin login-password: 123456 # Redis配置 redis: host: 192.168.20.51 port: 6379 password: AES:c8d9cdfddcb4b32c677d657d2c8d56f9e7e4720832656637f5 database: 4 timeout: 10000ms lettuce: pool: max-active: 8 max-wait: -1ms max-idle: 8 min-idle: 0 # MinIO配置 minio: endpoint: http://192.168.20.52:9000 access-key: minioadmin secret-key: AES:c4d4cefddd95e6733df755af0e29839c104ee8baa55eb53cdc24 part-size: 104857600 bucket-name: dev