feat:配置加密机制
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
package com.cisd.tms;
|
||||
|
||||
import com.cisd.tms.common.crypto.config.ConfigCryptoCli;
|
||||
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@@ -11,6 +12,13 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
public class TmsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
if (ConfigCryptoCli.shouldHandle(args)) {
|
||||
int exitCode = ConfigCryptoCli.run(args, System.in, System.out, System.err);
|
||||
if (exitCode != 0) {
|
||||
System.exit(exitCode);
|
||||
}
|
||||
return;
|
||||
}
|
||||
SpringApplication.run(TmsApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.cisd.tms.common.crypto.config;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class ConfigCryptoCli {
|
||||
|
||||
private static final String ENCRYPT_ARG = "--tms.crypto.encrypt";
|
||||
private static final String DECRYPT_ARG = "--tms.crypto.decrypt";
|
||||
private static final String VALUE_ARG_PREFIX = "--tms.crypto.value=";
|
||||
|
||||
private ConfigCryptoCli() {
|
||||
}
|
||||
|
||||
public static boolean shouldHandle(String[] args) {
|
||||
return contains(args, ENCRYPT_ARG) || contains(args, DECRYPT_ARG);
|
||||
}
|
||||
|
||||
public static int run(String[] args, InputStream in, PrintStream out, PrintStream err) {
|
||||
boolean encrypt = contains(args, ENCRYPT_ARG);
|
||||
boolean decrypt = contains(args, DECRYPT_ARG);
|
||||
if (encrypt == decrypt) {
|
||||
err.println("Usage: --tms.crypto.encrypt|--tms.crypto.decrypt [--tms.crypto.value=<value>]");
|
||||
return 2;
|
||||
}
|
||||
try {
|
||||
String input = valueFromArgs(args);
|
||||
if (input == null) {
|
||||
input = readLine(in);
|
||||
}
|
||||
ConfigTextEncryptor encryptor = ConfigTextEncryptor.withDefaultHardcodedKey();
|
||||
out.println(encrypt ? encryptor.encrypt(input) : encryptor.decryptIfNecessary(input));
|
||||
return 0;
|
||||
} catch (Exception ex) {
|
||||
err.println(ex.getMessage());
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean contains(String[] args, String expected) {
|
||||
return Arrays.stream(args == null ? new String[0] : args).anyMatch(expected::equals);
|
||||
}
|
||||
|
||||
private static String valueFromArgs(String[] args) {
|
||||
if (args == null) {
|
||||
return null;
|
||||
}
|
||||
for (String arg : args) {
|
||||
if (arg != null && arg.startsWith(VALUE_ARG_PREFIX)) {
|
||||
return arg.substring(VALUE_ARG_PREFIX.length());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String readLine(InputStream in) throws IOException {
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
String line = reader.readLine();
|
||||
if (line == null) {
|
||||
throw new IllegalArgumentException("请输入待处理文本");
|
||||
}
|
||||
return line;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.cisd.tms.common.crypto.config;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
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.PropertySource;
|
||||
|
||||
public class ConfigDecryptingEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
|
||||
|
||||
private static final String DECRYPTED_PROPERTY_SOURCE_NAME = "tmsConfigDecryptedProperties";
|
||||
|
||||
private final ConfigTextEncryptor encryptor;
|
||||
|
||||
public ConfigDecryptingEnvironmentPostProcessor() {
|
||||
this(ConfigTextEncryptor.withDefaultHardcodedKey());
|
||||
}
|
||||
|
||||
ConfigDecryptingEnvironmentPostProcessor(ConfigTextEncryptor encryptor) {
|
||||
this.encryptor = encryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
||||
Map<String, Object> decryptedValues = new LinkedHashMap<>();
|
||||
for (PropertySource<?> propertySource : environment.getPropertySources()) {
|
||||
if (!(propertySource instanceof EnumerablePropertySource<?> enumerablePropertySource)) {
|
||||
continue;
|
||||
}
|
||||
for (String propertyName : enumerablePropertySource.getPropertyNames()) {
|
||||
Object rawValue = enumerablePropertySource.getProperty(propertyName);
|
||||
if (rawValue instanceof String stringValue && encryptor.isEncrypted(stringValue)) {
|
||||
decryptedValues.putIfAbsent(propertyName, encryptor.decryptIfNecessary(stringValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!decryptedValues.isEmpty()) {
|
||||
environment.getPropertySources().addFirst(new MapPropertySource(DECRYPTED_PROPERTY_SOURCE_NAME, decryptedValues));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.cisd.tms.common.crypto.config;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class ConfigTextEncryptor {
|
||||
|
||||
private static final String ENCRYPTED_PREFIX = "ENC(";
|
||||
private static final String ENCRYPTED_SUFFIX = ")";
|
||||
private static final String FORMAT_VERSION = "v1";
|
||||
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
private static final String KEY_ALGORITHM = "AES";
|
||||
private static final int IV_LENGTH_BYTES = 12;
|
||||
private static final int GCM_TAG_LENGTH_BITS = 128;
|
||||
private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding();
|
||||
private static final Base64.Decoder DECODER = Base64.getUrlDecoder();
|
||||
|
||||
// Temporary fixed key required by the current delivery request. Replace with an external key source later.
|
||||
private static final String HARDCODED_KEY_BASE64 = "LMJI3dMjle5U9sMH/tlX1/1mOuVAYsg+cPsxGppFua0=";
|
||||
|
||||
private final SecretKeySpec keySpec;
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
public static ConfigTextEncryptor withDefaultHardcodedKey() {
|
||||
return new ConfigTextEncryptor(Base64.getDecoder().decode(HARDCODED_KEY_BASE64), new SecureRandom());
|
||||
}
|
||||
|
||||
ConfigTextEncryptor(byte[] keyBytes, SecureRandom secureRandom) {
|
||||
if (keyBytes == null || keyBytes.length != 32) {
|
||||
throw new IllegalArgumentException("配置加密密钥必须是32字节AES-256密钥");
|
||||
}
|
||||
this.keySpec = new SecretKeySpec(keyBytes, KEY_ALGORITHM);
|
||||
this.secureRandom = secureRandom;
|
||||
}
|
||||
|
||||
public String encrypt(String plainText) {
|
||||
if (plainText == null) {
|
||||
throw new IllegalArgumentException("待加密明文不能为空");
|
||||
}
|
||||
byte[] iv = new byte[IV_LENGTH_BYTES];
|
||||
secureRandom.nextBytes(iv);
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
|
||||
byte[] ciphertext = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return ENCRYPTED_PREFIX
|
||||
+ FORMAT_VERSION
|
||||
+ ":"
|
||||
+ ENCODER.encodeToString(iv)
|
||||
+ ":"
|
||||
+ ENCODER.encodeToString(ciphertext)
|
||||
+ ENCRYPTED_SUFFIX;
|
||||
} catch (GeneralSecurityException ex) {
|
||||
throw new IllegalStateException("配置加密失败", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public String decryptIfNecessary(String value) {
|
||||
if (value == null || !isEncrypted(value)) {
|
||||
return value;
|
||||
}
|
||||
return decrypt(value);
|
||||
}
|
||||
|
||||
public boolean isEncrypted(String value) {
|
||||
return value != null && value.startsWith(ENCRYPTED_PREFIX) && value.endsWith(ENCRYPTED_SUFFIX);
|
||||
}
|
||||
|
||||
private String decrypt(String encryptedValue) {
|
||||
String payload = encryptedValue.substring(ENCRYPTED_PREFIX.length(), encryptedValue.length() - ENCRYPTED_SUFFIX.length());
|
||||
String[] parts = payload.split(":", -1);
|
||||
if (parts.length != 3 || !FORMAT_VERSION.equals(parts[0])) {
|
||||
throw new IllegalArgumentException("配置密文格式无效");
|
||||
}
|
||||
try {
|
||||
byte[] iv = DECODER.decode(parts[1]);
|
||||
byte[] ciphertext = DECODER.decode(parts[2]);
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, iv));
|
||||
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
|
||||
} catch (IllegalArgumentException | GeneralSecurityException ex) {
|
||||
throw new IllegalArgumentException("配置密文解密失败", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.env.EnvironmentPostProcessor=\
|
||||
com.cisd.tms.common.crypto.config.ConfigDecryptingEnvironmentPostProcessor
|
||||
@@ -1,8 +1,8 @@
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://127.0.0.1:4000/tms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
|
||||
username: root
|
||||
password: Sunyard123
|
||||
username: ENC(v1:XBsGZZ5ugvSBy0HZ:nvihsBsr4b9HhHYGTDbVlqy7kT0)
|
||||
password: ENC(v1:K_VO3NW0dq59UjJh:KcngqSlmeiCP0VXGwkB031xOMjE4ktoz6vo)
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
|
||||
mybatis-plus:
|
||||
|
||||
@@ -182,6 +182,9 @@ tms:
|
||||
memory-total-default: ${TMS_DEVICE_PROFILE_MEMORY_TOTAL_DEFAULT:16GB}
|
||||
# 磁盘总量默认展示值;命令读取失败或为空时回退。
|
||||
disk-total-default: ${TMS_DEVICE_PROFILE_DISK_TOTAL_DEFAULT:256G}
|
||||
runtime-status:
|
||||
# 运行状态磁盘已用量探测路径;总量使用 lsblk 统计 OS 可见物理磁盘容量。
|
||||
disk-usage-path: ${TMS_DEVICE_RUNTIME_STATUS_DISK_USAGE_PATH:/home/tms}
|
||||
upgrade:
|
||||
# 升级包解压和脚本执行暂存目录。
|
||||
staging-root-dir: ${TMS_UPGRADE_STAGING_ROOT_DIR:/home/tmp/tms-upgrade-staging}
|
||||
@@ -300,17 +303,22 @@ tms:
|
||||
mysql-path: ${TMS_BACKUP_MYSQL_PATH:mysql}
|
||||
# 数据库恢复脚本路径;apply 脚本在 RESTORE_DATABASES 阶段调用。
|
||||
restore-db-script-path: ${TMS_BACKUP_RESTORE_DB_SCRIPT_PATH:/home/tms/bin/resource-restore/restore-db.sh}
|
||||
# 单个数据库恢复最大等待时间(秒);超时会标记恢复失败并尝试重新启动服务。
|
||||
restore-db-timeout-seconds: ${TMS_BACKUP_RESTORE_DB_TIMEOUT_SECONDS:300}
|
||||
# MQ replay 脚本路径;apply 脚本在 MQ_REPLAY 阶段调用。
|
||||
mq-replay-script-path: ${TMS_BACKUP_MQ_REPLAY_SCRIPT_PATH:/home/tms/bin/resource-restore/replay-mq.sh}
|
||||
# 备份 TMS 库名;为空时默认从 spring.datasource.url 中解析。
|
||||
tms-database-name: ${TMS_BACKUP_TMS_DATABASE_NAME:}
|
||||
# TMS 库备份排除表。默认不备份角色、授权、会话和审计表,避免恢复时覆盖新机器安全状态。
|
||||
# TMS 库备份排除表。默认不备份角色、授权、会话和资源备份/恢复任务表,避免恢复时覆盖新机器安全登录状态或带回旧 RUNNING 任务。
|
||||
tms-db-excluded-tables:
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_ROLE_ACCOUNT:tms_role_account}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_ROLE_UKEY:tms_role_ukey_binding}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_FULL:tms_auth_full_account}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_SESSION:tms_auth_session}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_CHALLENGE:tms_auth_challenge}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_AUTH_AUDIT:tms_auth_audit_log}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_RESOURCE_BACKUP_TASK:tms_resource_backup_task}
|
||||
- ${TMS_BACKUP_TMS_DB_EXCLUDED_TABLE_RESOURCE_RESTORE_TASK:tms_resource_restore_task}
|
||||
# 备份/恢复标准收发器业务库名;第一版默认使用 CMEP。
|
||||
cmep-database-name: ${TMS_BACKUP_CMEP_DATABASE_NAME:CMEP}
|
||||
# 恢复完成后的最小健康检查地址。
|
||||
@@ -341,7 +349,7 @@ tms:
|
||||
debug-session-token: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_SESSION_TOKEN:DEBUG-BYPASS}
|
||||
replay:
|
||||
# 是否启用防重放校验;本地 Postman/联调可临时设为 false,生产环境应保持 true。
|
||||
enabled: ${TMS_SECURITY_REPLAY_ENABLED:false}
|
||||
enabled: ${TMS_SECURITY_REPLAY_ENABLED:true}
|
||||
openapi:
|
||||
# 外部签名服务接口允许的时间戳偏差(秒),防重放。
|
||||
timestamp-skew-seconds: 300
|
||||
|
||||
Reference in New Issue
Block a user