refactor: 配置刷新优化和加密功能移除
- refactor(springboot2): 简化配置刷新逻辑,移除 EnvironmentChangeEvent 包装 - ConfigChangeHanlder 直接接收 Map<String,String> 参数 - 移除 EnvironmentChangeEvent 事件类 - 优化 token 参数构建和敏感信息脱敏处理 - refactor(server): 移除配置加密功能 - 删除 CryptoConfig 结构体和相关配置项 - 移除 application-dev.properties 中的加密配置 - 项目不再使用配置加密功能 - docs: 更新 README 和模块文档 - fix: 修复 Node.js 客户端配置缓存
This commit is contained in:
@@ -64,7 +64,6 @@ database.user=root
|
|||||||
database.password=your_password
|
database.password=your_password
|
||||||
jwt.secret=your-jwt-secret
|
jwt.secret=your-jwt-secret
|
||||||
jwt.expireHours=24
|
jwt.expireHours=24
|
||||||
crypto.secretKey=your-crypto-key
|
|
||||||
```
|
```
|
||||||
|
|
||||||
启动服务(首次启动自动建表并初始化默认数据):
|
启动服务(首次启动自动建表并初始化默认数据):
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"crypto.secretKey": "k9#mP2$vL7qR4xN8",
|
|
||||||
"database.host": "127.0.0.1",
|
"database.host": "127.0.0.1",
|
||||||
"database.name": "kenaito_conf",
|
"database.name": "kenaito_conf",
|
||||||
"database.password": "1234567",
|
"database.password": "1234567",
|
||||||
|
|||||||
+1
-1
@@ -4,5 +4,5 @@ import java.util.Map;
|
|||||||
|
|
||||||
public interface ConfigChangeHanlder {
|
public interface ConfigChangeHanlder {
|
||||||
|
|
||||||
public void onConfigChanged(Map<String, Object> changedConfigs);
|
void onConfigChanged(Map<String, Object> changedConfigs);
|
||||||
}
|
}
|
||||||
|
|||||||
+447
-537
@@ -7,12 +7,6 @@ import java.security.MessageDigest;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.commons.lang3.Validate;
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -34,535 +28,451 @@ import com.alibaba.fastjson2.JSON;
|
|||||||
*/
|
*/
|
||||||
public class ConfigcenterContext {
|
public class ConfigcenterContext {
|
||||||
|
|
||||||
private final static Logger logger = LoggerFactory.getLogger("cn.odboy");
|
private final static Logger logger = LoggerFactory.getLogger("cn.odboy");
|
||||||
|
|
||||||
private static ConfigcenterContext instance = new ConfigcenterContext();
|
private static ConfigcenterContext instance = new ConfigcenterContext();
|
||||||
|
|
||||||
private List<String> sensitiveKeys = new ArrayList<>(Arrays.asList("password", "key", "secret", "token", "credentials"));
|
private List<String> sensitiveKeys = new ArrayList<>(Arrays.asList("password", "key", "secret", "token", "credentials"));
|
||||||
|
|
||||||
public static final String MANAGER_PROPERTY_SOURCE = "kenaitoConfigPropertySource";
|
public static final String MANAGER_PROPERTY_SOURCE = "kenaitoConfigPropertySource";
|
||||||
|
|
||||||
private static final String IGNORE_PLACEHOLER = "[Ignore]";
|
private static final String IGNORE_PLACEHOLER = "[Ignore]";
|
||||||
private static final String CRYPT_PREFIX = "{Cipher}";
|
private static final String CRYPT_PREFIX = "{Cipher}";
|
||||||
|
|
||||||
// Local property storage (replaces ResourceUtils)
|
// Local property storage (replaces ResourceUtils)
|
||||||
private static final Map<String, String> localProps = new ConcurrentHashMap<>();
|
private static final Map<String, String> localProps = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
// Spring ApplicationContext (replaces InstanceFactory)
|
// Spring ApplicationContext (replaces InstanceFactory)
|
||||||
private static ApplicationContext applicationContext;
|
private static ApplicationContext applicationContext;
|
||||||
|
|
||||||
private Boolean remoteEnabled;
|
private Boolean remoteEnabled;
|
||||||
private final String nodeId;
|
private final String nodeId;
|
||||||
private String[] apiBaseUrls;
|
private String[] apiBaseUrls;
|
||||||
private String app;
|
private String app;
|
||||||
private String env;
|
private String env;
|
||||||
private String version;
|
private String version;
|
||||||
private boolean ignoreGlobal;
|
private boolean ignoreGlobal;
|
||||||
private String secret;
|
private String secret;
|
||||||
private String globalSecret;
|
private String globalSecret;
|
||||||
private String token;
|
private String token;
|
||||||
private boolean remoteFirst = false;
|
private boolean remoteFirst = false;
|
||||||
private boolean isSpringboot;
|
private boolean isSpringboot;
|
||||||
private int syncIntervalSeconds = 50;
|
private int syncIntervalSeconds = 50;
|
||||||
private InternalConfigChangeListener configChangeListener;
|
private InternalConfigChangeListener configChangeListener;
|
||||||
private List<ConfigChangeHanlder> configChangeHanlders;
|
private List<ConfigChangeHanlder> configChangeHanlders;
|
||||||
private boolean processed;
|
private boolean processed;
|
||||||
private Properties remoteProperties;
|
private Properties remoteProperties;
|
||||||
|
|
||||||
private ConfigcenterContext() {
|
private ConfigcenterContext() {
|
||||||
String name;
|
String name;
|
||||||
try {
|
try {
|
||||||
name = InetAddress.getLocalHost().getHostName();
|
name = InetAddress.getLocalHost().getHostName();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
name = "unknown";
|
name = "unknown";
|
||||||
}
|
}
|
||||||
this.nodeId = name;
|
this.nodeId = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ConfigcenterContext getInstance() {
|
public static ConfigcenterContext getInstance() {
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setApplicationContext(ApplicationContext ctx) {
|
public static void setApplicationContext(ApplicationContext ctx) {
|
||||||
applicationContext = ctx;
|
applicationContext = ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ApplicationContext getApplicationContext() {
|
public static ApplicationContext getApplicationContext() {
|
||||||
return applicationContext;
|
return applicationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Property Utils (replaces ResourceUtils) ====================
|
// ==================== Property Utils (replaces ResourceUtils) ====================
|
||||||
|
|
||||||
public static void mergeProperties(Properties props) {
|
public static void mergeProperties(Properties props) {
|
||||||
if (props != null) {
|
if (props != null) {
|
||||||
for (Entry<Object, Object> entry : props.entrySet()) {
|
for (Entry<Object, Object> entry : props.entrySet()) {
|
||||||
localProps.put(entry.getKey().toString(), entry.getValue().toString());
|
localProps.put(entry.getKey().toString(), entry.getValue().toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void addProperty(String key, String value) {
|
public static void addProperty(String key, String value) {
|
||||||
localProps.put(key, value);
|
localProps.put(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getProperty(String key) {
|
public static String getProperty(String key) {
|
||||||
String val = localProps.get(key);
|
String val = localProps.get(key);
|
||||||
if (val == null) val = System.getProperty(key);
|
if (val == null) val = System.getProperty(key);
|
||||||
if (val == null) val = System.getenv(key);
|
if (val == null) val = System.getenv(key);
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getProperty(String key, String defaultValue) {
|
public static String getProperty(String key, String defaultValue) {
|
||||||
String val = getProperty(key);
|
String val = getProperty(key);
|
||||||
return val != null && !val.isEmpty() ? val : defaultValue;
|
return val != null && !val.isEmpty() ? val : defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean getBoolean(String key, boolean defaultValue) {
|
public static boolean getBoolean(String key, boolean defaultValue) {
|
||||||
String val = getProperty(key);
|
String val = getProperty(key);
|
||||||
if (val == null || val.isEmpty()) return defaultValue;
|
if (val == null || val.isEmpty()) return defaultValue;
|
||||||
return Boolean.parseBoolean(val);
|
return Boolean.parseBoolean(val);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getInt(String key, int defaultValue) {
|
public static int getInt(String key, int defaultValue) {
|
||||||
String val = getProperty(key);
|
String val = getProperty(key);
|
||||||
if (val == null || val.isEmpty()) return defaultValue;
|
if (val == null || val.isEmpty()) return defaultValue;
|
||||||
try {
|
try {
|
||||||
return Integer.parseInt(val);
|
return Integer.parseInt(val);
|
||||||
} catch (NumberFormatException e) {
|
} catch (NumberFormatException e) {
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getAnyProperty(String... keys) {
|
public static String getAnyProperty(String... keys) {
|
||||||
for (String key : keys) {
|
for (String key : keys) {
|
||||||
String val = getProperty(key);
|
String val = getProperty(key);
|
||||||
if (val != null && !val.isEmpty()) return val;
|
if (val != null && !val.isEmpty()) return val;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Crypto Utils (replaces mendmix AES/DES/Base64/DigestUtils) ====================
|
// ==================== HTTP Utils (replaces mendmix HttpUtils) ====================
|
||||||
|
|
||||||
public static String md5(String input) {
|
public static String httpGet(String url) throws Exception {
|
||||||
try {
|
return HttpClientUtils.get(url);
|
||||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
}
|
||||||
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
|
|
||||||
StringBuilder sb = new StringBuilder();
|
public static String httpPostJson(String url, String json) throws Exception {
|
||||||
for (byte b : digest) {
|
return HttpClientUtils.postJson(url, json);
|
||||||
sb.append(String.format("%02x", b));
|
}
|
||||||
}
|
|
||||||
return sb.toString();
|
// ==================== Core Methods ====================
|
||||||
} catch (Exception e) {
|
|
||||||
throw new RuntimeException("MD5 error", e);
|
public boolean isProcessed() {
|
||||||
}
|
return processed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String md5Short(String input) {
|
public synchronized void init(Properties properties, boolean isSpringboot) {
|
||||||
if (input == null || input.isEmpty()) return "";
|
if (processed || !isRemoteEnabled()) return;
|
||||||
return md5(input).substring(0, 16);
|
if (properties != null) mergeProperties(properties);
|
||||||
}
|
|
||||||
|
System.setProperty("client.nodeId", nodeId);
|
||||||
private static String desEncrypt(String key, String data) {
|
System.setProperty("springboot", String.valueOf(isSpringboot));
|
||||||
try {
|
this.isSpringboot = isSpringboot;
|
||||||
SecretKey secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "DES");
|
|
||||||
Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
|
String defaultAppName = getProperty("spring.application.name");
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
app = getProperty("kenaito.config.appName", defaultAppName);
|
||||||
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
if (remoteEnabled == null) remoteEnabled = getBoolean("kenaito.config.enabled", true);
|
||||||
return Base64.getEncoder().encodeToString(encrypted);
|
|
||||||
} catch (Exception e) {
|
if (!isRemoteEnabled()) return;
|
||||||
throw new RuntimeException("DES encrypt error", e);
|
|
||||||
}
|
env = getProperty("kenaito.config.profile", "dev");
|
||||||
}
|
Validate.notBlank(env, "[kenaito.config.profile] is required");
|
||||||
|
|
||||||
private static byte[] aesDecrypt(byte[] data, byte[] key) throws Exception {
|
setApiBaseUrl(getAnyProperty("kenaito.config.baseUrl", "kenaito.config.baseUrl"));
|
||||||
SecretKey secretKey = new SecretKeySpec(key, "AES");
|
|
||||||
// Use CBC mode with zero IV to match Go server
|
version = getProperty("kenaito.config.version", "latest");
|
||||||
IvParameterSpec iv = new IvParameterSpec(new byte[16]);
|
ignoreGlobal = getBoolean("kenaito.config.global-ignore", false);
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
|
syncIntervalSeconds = getInt("kenaito.config.sync-interval-seconds", 60);
|
||||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
|
token = getProperty("kenaito.config.token");
|
||||||
byte[] decrypted = cipher.doFinal(data);
|
|
||||||
// Remove PKCS7 padding
|
logger.info("\n=====Configcenter config=====\nappName:{}\nenv:{}\nversion:{}\nremoteEnabled:{}\napiBaseUrls:{}\n=====Configcenter config=====", app, env, version, isRemoteEnabled(), JSON.toJSONString(apiBaseUrls));
|
||||||
int padding = decrypted[decrypted.length - 1] & 0xFF;
|
}
|
||||||
if (padding > 0 && padding <= 16) {
|
|
||||||
byte[] result = new byte[decrypted.length - padding];
|
public boolean isRemoteEnabled() {
|
||||||
System.arraycopy(decrypted, 0, result, 0, result.length);
|
return remoteEnabled == null || remoteEnabled;
|
||||||
return result;
|
}
|
||||||
}
|
|
||||||
return decrypted;
|
public void setRemoteEnabled(boolean remoteEnabled) {
|
||||||
}
|
this.remoteEnabled = remoteEnabled;
|
||||||
|
}
|
||||||
private static String decryptWithAES(String key, String data) {
|
|
||||||
try {
|
public String[] getApiBaseUrls() {
|
||||||
String secretKey = md5(key).substring(16);
|
return apiBaseUrls;
|
||||||
byte[] encrypted = Base64.getDecoder().decode(data.getBytes(StandardCharsets.UTF_8));
|
}
|
||||||
byte[] decrypted = aesDecrypt(encrypted, secretKey.getBytes(StandardCharsets.UTF_8));
|
|
||||||
return new String(decrypted, StandardCharsets.UTF_8);
|
public void setApiBaseUrl(String apiBaseUrl) {
|
||||||
} catch (Exception e) {
|
Validate.notBlank(apiBaseUrl, "[kenaito.config.baseUrl] is required");
|
||||||
throw new RuntimeException(String.format("解密错误:%s", data));
|
String[] urls = apiBaseUrl.split(",|;");
|
||||||
}
|
this.apiBaseUrls = new String[urls.length];
|
||||||
}
|
for (int i = 0; i < urls.length; i++) {
|
||||||
|
if (urls[i].endsWith("/")) {
|
||||||
// ==================== HTTP Utils (replaces mendmix HttpUtils) ====================
|
this.apiBaseUrls[i] = urls[i].substring(0, urls[i].length() - 1);
|
||||||
|
} else {
|
||||||
public static String httpGet(String url) throws Exception {
|
this.apiBaseUrls[i] = urls[i];
|
||||||
return HttpClientUtils.get(url);
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
public static String httpPostJson(String url, String json) throws Exception {
|
|
||||||
return HttpClientUtils.postJson(url, json);
|
public String getApp() {
|
||||||
}
|
return app;
|
||||||
|
}
|
||||||
// ==================== Core Methods ====================
|
|
||||||
|
public String getEnv() {
|
||||||
public boolean isProcessed() {
|
return env;
|
||||||
return processed;
|
}
|
||||||
}
|
|
||||||
|
public String getVersion() {
|
||||||
public synchronized void init(Properties properties, boolean isSpringboot) {
|
return version;
|
||||||
if (processed || !isRemoteEnabled()) return;
|
}
|
||||||
if (properties != null) mergeProperties(properties);
|
|
||||||
|
public boolean isIgnoreGlobal() {
|
||||||
System.setProperty("client.nodeId", nodeId);
|
return ignoreGlobal;
|
||||||
System.setProperty("springboot", String.valueOf(isSpringboot));
|
}
|
||||||
this.isSpringboot = isSpringboot;
|
|
||||||
|
public boolean isRemoteFirst() {
|
||||||
String defaultAppName = getProperty("spring.application.name");
|
return remoteFirst;
|
||||||
app = getProperty("kenaito.config.appName", defaultAppName);
|
}
|
||||||
if (remoteEnabled == null) remoteEnabled = getBoolean("kenaito.config.enabled", true);
|
|
||||||
|
public int getSyncIntervalSeconds() {
|
||||||
if (!isRemoteEnabled()) return;
|
return syncIntervalSeconds;
|
||||||
|
}
|
||||||
env = getProperty("kenaito.config.profile", "dev");
|
|
||||||
Validate.notBlank(env, "[kenaito.config.profile] is required");
|
public void mergeRemoteProperties(Properties properties) {
|
||||||
|
if (!remoteEnabled) return;
|
||||||
setApiBaseUrl(getAnyProperty("kenaito.config.baseUrl", "kenaito.config.baseUrl"));
|
remoteProperties = getAllRemoteProperties();
|
||||||
|
if (remoteProperties != null) {
|
||||||
version = getProperty("kenaito.config.version", "latest");
|
Set<Entry<Object, Object>> entrySet = remoteProperties.entrySet();
|
||||||
ignoreGlobal = getBoolean("kenaito.config.global-ignore", false);
|
for (Entry<Object, Object> entry : entrySet) {
|
||||||
syncIntervalSeconds = getInt("kenaito.config.sync-interval-seconds", 60);
|
if (!isRemoteFirst() && properties.containsKey(entry.getKey())) {
|
||||||
token = getProperty("kenaito.config.token");
|
continue;
|
||||||
|
}
|
||||||
logger.info("\n=====Configcenter config=====\nappName:{}\nenv:{}\nversion:{}\nremoteEnabled:{}\napiBaseUrls:{}\n=====Configcenter config=====",
|
String value = entry.getValue().toString();
|
||||||
app, env, version, isRemoteEnabled(), JSON.toJSONString(apiBaseUrls));
|
if (IGNORE_PLACEHOLER.equals(value)) continue;
|
||||||
}
|
properties.setProperty(entry.getKey().toString(), value);
|
||||||
|
}
|
||||||
public boolean isRemoteEnabled() {
|
}
|
||||||
return remoteEnabled == null || remoteEnabled;
|
|
||||||
}
|
// 替换本地变量占位符
|
||||||
|
Set<Entry<Object, Object>> entrySet = properties.entrySet();
|
||||||
public void setRemoteEnabled(boolean remoteEnabled) {
|
for (Entry<Object, Object> entry : entrySet) {
|
||||||
this.remoteEnabled = remoteEnabled;
|
String key = entry.getKey().toString();
|
||||||
}
|
String value = entry.getValue().toString();
|
||||||
|
addProperty(key, value);
|
||||||
public String[] getApiBaseUrls() {
|
}
|
||||||
return apiBaseUrls;
|
|
||||||
}
|
// 加载本地测试配置
|
||||||
|
String path = System.getProperty("localtest.config.location");
|
||||||
public void setApiBaseUrl(String apiBaseUrl) {
|
if (StringUtils.isNotBlank(path)) {
|
||||||
Validate.notBlank(apiBaseUrl, "[kenaito.config.baseUrl] is required");
|
try (FileReader fileReader = new FileReader(path)) {
|
||||||
String[] urls = apiBaseUrl.split(",|;");
|
Properties localProperties = new Properties();
|
||||||
this.apiBaseUrls = new String[urls.length];
|
localProperties.load(fileReader);
|
||||||
for (int i = 0; i < urls.length; i++) {
|
if (!localProperties.isEmpty()) {
|
||||||
if (urls[i].endsWith("/")) {
|
properties.putAll(localProperties);
|
||||||
this.apiBaseUrls[i] = urls[i].substring(0, urls[i].length() - 1);
|
}
|
||||||
} else {
|
} catch (Exception e) {
|
||||||
this.apiBaseUrls[i] = urls[i];
|
logger.warn("Failed to load local test config from {}: {}", path, e.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Close existing listener before creating new one
|
||||||
public String getApp() { return app; }
|
if (configChangeListener != null) {
|
||||||
public String getEnv() { return env; }
|
configChangeListener.close();
|
||||||
public String getVersion() { return version; }
|
}
|
||||||
public boolean isIgnoreGlobal() { return ignoreGlobal; }
|
// register listener
|
||||||
public String getSecret() { return secret; }
|
configChangeListener = new InternalConfigChangeListener();
|
||||||
public boolean isRemoteFirst() { return remoteFirst; }
|
printConfigs(properties);
|
||||||
public int getSyncIntervalSeconds() { return syncIntervalSeconds; }
|
}
|
||||||
public String getNodeId() { return nodeId; }
|
|
||||||
public boolean isSpringboot() { return isSpringboot; }
|
private Properties getAllRemoteProperties() {
|
||||||
|
if (remoteProperties != null) return remoteProperties;
|
||||||
public void mergeRemoteProperties(Properties properties) {
|
Properties properties = new Properties();
|
||||||
if (!remoteEnabled) return;
|
|
||||||
remoteProperties = getAllRemoteProperties();
|
Map<String, Object> map = fetchConfigFromServer();
|
||||||
if (remoteProperties != null) {
|
if (map == null) {
|
||||||
Set<Entry<Object, Object>> entrySet = remoteProperties.entrySet();
|
throw new RuntimeException("fetch remote config error!");
|
||||||
for (Entry<Object, Object> entry : entrySet) {
|
}
|
||||||
if (!isRemoteFirst() && properties.containsKey(entry.getKey())) {
|
|
||||||
continue;
|
// 解密密匙
|
||||||
}
|
secret = Objects.toString(map.remove("kenaito.config.encrypt-secret"), null);
|
||||||
String value = entry.getValue().toString();
|
globalSecret = Objects.toString(map.remove("kenaito.config.global-encrypt-secret"), null);
|
||||||
if (IGNORE_PLACEHOLER.equals(value)) continue;
|
remoteFirst = Boolean.parseBoolean(Objects.toString(map.remove("kenaito.config.remote-config-first"), "false"));
|
||||||
properties.setProperty(entry.getKey().toString(), value);
|
|
||||||
}
|
Set<String> keys = map.keySet();
|
||||||
}
|
for (String key : keys) {
|
||||||
|
Object value = map.get(key);
|
||||||
// 替换本地变量占位符
|
properties.put(key, value);
|
||||||
Set<Entry<Object, Object>> entrySet = properties.entrySet();
|
}
|
||||||
for (Entry<Object, Object> entry : entrySet) {
|
|
||||||
String key = entry.getKey().toString();
|
return properties;
|
||||||
String value = entry.getValue().toString();
|
}
|
||||||
addProperty(key, value);
|
|
||||||
}
|
@SuppressWarnings("unchecked")
|
||||||
|
private Map<String, Object> fetchConfigFromServer() {
|
||||||
// 加载本地测试配置
|
Map<String, Object> result = null;
|
||||||
String path = System.getProperty("localtest.config.location");
|
String errorMsg = null;
|
||||||
if (StringUtils.isNotBlank(path)) {
|
for (String apiBaseUrl : apiBaseUrls) {
|
||||||
try (FileReader fileReader = new FileReader(path)) {
|
String url = apiBaseUrl + "/api/fetchAllConfigs";
|
||||||
Properties localProperties = new Properties();
|
Map<String, Object> params = new HashMap<>();
|
||||||
localProperties.load(fileReader);
|
params.put("appName", app);
|
||||||
if (!localProperties.isEmpty()) {
|
params.put("env", env);
|
||||||
properties.putAll(localProperties);
|
params.put("version", version);
|
||||||
}
|
params.put("ignoreGlobal", ignoreGlobal);
|
||||||
} catch (Exception e) {
|
if (token != null && !token.isEmpty()) {
|
||||||
logger.warn("Failed to load local test config from {}: {}", path, e.getMessage());
|
params.put("token", token);
|
||||||
}
|
}
|
||||||
}
|
logger.info("fetch configs url:{}", url);
|
||||||
|
try {
|
||||||
// Close existing listener before creating new one
|
String jsonString = httpPostJson(url, JSON.toJSONString(params));
|
||||||
if (configChangeListener != null) {
|
if (jsonString != null) {
|
||||||
configChangeListener.close();
|
result = JSON.parseObject(jsonString, Map.class);
|
||||||
}
|
if (result.containsKey("code")) {
|
||||||
// register listener
|
errorMsg = result.get("msg").toString();
|
||||||
configChangeListener = new InternalConfigChangeListener();
|
logger.error("fetch error:{}", errorMsg);
|
||||||
printConfigs(properties);
|
result = null;
|
||||||
}
|
} else {
|
||||||
|
break;
|
||||||
private Properties getAllRemoteProperties() {
|
}
|
||||||
if (remoteProperties != null) return remoteProperties;
|
}
|
||||||
Properties properties = new Properties();
|
} catch (Exception e) {
|
||||||
|
logger.error("fetch config error from {}: {}", apiBaseUrl, e.getMessage());
|
||||||
Map<String, Object> map = fetchConfigFromServer();
|
}
|
||||||
if (map == null) {
|
}
|
||||||
throw new RuntimeException("fetch remote config error!");
|
|
||||||
}
|
if (result == null) {
|
||||||
|
logger.warn(">>>>>remote config fetch error, load from local Cache");
|
||||||
// 解密密匙
|
result = LocalCacheUtils.read();
|
||||||
secret = Objects.toString(map.remove("kenaito.config.encrypt-secret"), null);
|
} else {
|
||||||
globalSecret = Objects.toString(map.remove("kenaito.config.global-encrypt-secret"), null);
|
LocalCacheUtils.write(result);
|
||||||
remoteFirst = Boolean.parseBoolean(Objects.toString(map.remove("kenaito.config.remote-config-first"), "false"));
|
}
|
||||||
|
return result;
|
||||||
Set<String> keys = map.keySet();
|
}
|
||||||
for (String key : keys) {
|
|
||||||
Object value = decodeEncryptIfRequire(map.get(key));
|
private void printConfigs(Properties properties) {
|
||||||
properties.put(key, value);
|
List<String> sortKeys = new ArrayList<>();
|
||||||
}
|
Set<Entry<Object, Object>> entrySet = properties.entrySet();
|
||||||
|
for (Entry<Object, Object> entry : entrySet) {
|
||||||
return properties;
|
sortKeys.add(entry.getKey().toString());
|
||||||
}
|
}
|
||||||
|
Collections.sort(sortKeys);
|
||||||
@SuppressWarnings("unchecked")
|
System.out.println("==================final config list start==================");
|
||||||
private Map<String, Object> fetchConfigFromServer() {
|
for (String key : sortKeys) {
|
||||||
Map<String, Object> result = null;
|
String value = hideSensitive(key, properties.getProperty(key));
|
||||||
String errorMsg = null;
|
System.out.printf("%s = %s%n", key, value);
|
||||||
for (String apiBaseUrl : apiBaseUrls) {
|
}
|
||||||
String url = apiBaseUrl + "/api/fetchAllConfigs";
|
System.out.println("==================final config list end====================");
|
||||||
Map<String, Object> params = new HashMap<>();
|
}
|
||||||
params.put("appName", app);
|
|
||||||
params.put("env", env);
|
@SuppressWarnings("unchecked")
|
||||||
params.put("version", version);
|
public synchronized void updateConfig(Map<String, Object> updateConfig) {
|
||||||
params.put("ignoreGlobal", ignoreGlobal);
|
if (!updateConfig.isEmpty()) {
|
||||||
if (token != null && !token.isEmpty()) {
|
Set<String> keySet = updateConfig.keySet();
|
||||||
params.put("token", token);
|
for (String key : keySet) {
|
||||||
}
|
String oldValue = getProperty(key);
|
||||||
logger.info("fetch configs url:{}", url);
|
addProperty(key, updateConfig.get(key).toString());
|
||||||
try {
|
logger.info("Config [{}] Change, oldValue:{}, newValue:{}", key, oldValue, updateConfig.get(key));
|
||||||
String jsonString = httpPostJson(url, JSON.toJSONString(params));
|
}
|
||||||
if (jsonString != null) {
|
|
||||||
result = JSON.parseObject(jsonString, Map.class);
|
// Update Spring Environment property source
|
||||||
if (result.containsKey("code")) {
|
if (applicationContext != null) {
|
||||||
errorMsg = result.get("msg").toString();
|
StandardEnvironment environment = (StandardEnvironment) applicationContext.getEnvironment();
|
||||||
logger.error("fetch error:{}", errorMsg);
|
MutablePropertySources propertySources = environment.getPropertySources();
|
||||||
result = null;
|
synchronized (propertySources) {
|
||||||
} else {
|
Properties properties = new Properties();
|
||||||
break;
|
if (propertySources.contains(MANAGER_PROPERTY_SOURCE)) {
|
||||||
}
|
Object source = propertySources.get(MANAGER_PROPERTY_SOURCE);
|
||||||
}
|
if (source instanceof MapPropertySource) {
|
||||||
} catch (Exception e) {
|
Map<String, Object> map = ((MapPropertySource) source).getSource();
|
||||||
logger.error("fetch config error from {}: {}", apiBaseUrl, e.getMessage());
|
for (Entry<String, Object> entry : map.entrySet()) {
|
||||||
}
|
properties.put(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
|
} else if (source instanceof PropertiesPropertySource) {
|
||||||
if (result == null) {
|
Map<String, Object> src = ((PropertiesPropertySource) source).getSource();
|
||||||
logger.warn(">>>>>remote config fetch error, load from local Cache");
|
for (Entry<String, Object> entry : src.entrySet()) {
|
||||||
result = LocalCacheUtils.read();
|
properties.put(entry.getKey(), entry.getValue().toString());
|
||||||
} else {
|
}
|
||||||
LocalCacheUtils.write(result);
|
}
|
||||||
}
|
}
|
||||||
return result;
|
for (Entry<String, Object> entry : updateConfig.entrySet()) {
|
||||||
}
|
properties.put(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
private void printConfigs(Properties properties) {
|
propertySources.replace(MANAGER_PROPERTY_SOURCE, new PropertiesPropertySource(MANAGER_PROPERTY_SOURCE, properties));
|
||||||
List<String> sortKeys = new ArrayList<>();
|
}
|
||||||
Set<Entry<Object, Object>> entrySet = properties.entrySet();
|
}
|
||||||
for (Entry<Object, Object> entry : entrySet) {
|
|
||||||
sortKeys.add(entry.getKey().toString());
|
if (configChangeHanlders == null) {
|
||||||
}
|
configChangeHanlders = new ArrayList<>();
|
||||||
Collections.sort(sortKeys);
|
if (applicationContext != null) {
|
||||||
System.out.println("==================final config list start==================");
|
Map<String, ConfigChangeHanlder> interfaces = applicationContext.getBeansOfType(ConfigChangeHanlder.class);
|
||||||
for (String key : sortKeys) {
|
if (interfaces != null) {
|
||||||
String value = hideSensitive(key, properties.getProperty(key));
|
configChangeHanlders.addAll(interfaces.values());
|
||||||
System.out.printf("%s = %s%n", key, value);
|
}
|
||||||
}
|
}
|
||||||
System.out.println("==================final config list end====================");
|
}
|
||||||
}
|
|
||||||
|
for (ConfigChangeHanlder handler : configChangeHanlders) {
|
||||||
@SuppressWarnings("unchecked")
|
try {
|
||||||
public synchronized void updateConfig(Map<String, Object> updateConfig) {
|
handler.onConfigChanged(updateConfig);
|
||||||
if (!updateConfig.isEmpty()) {
|
logger.info("invoke {}.onConfigChanged successed!", handler.getClass().getName());
|
||||||
Set<String> keySet = updateConfig.keySet();
|
} catch (Exception e) {
|
||||||
for (String key : keySet) {
|
logger.warn("invoke {}.onConfigChanged error, msg:{}", handler.getClass().getName(), e.getMessage());
|
||||||
String oldValue = getProperty(key);
|
}
|
||||||
addProperty(key, decodeEncryptIfRequire(updateConfig.get(key)).toString());
|
}
|
||||||
logger.info("Config [{}] Change, oldValue:{}, newValue:{}", key, oldValue, updateConfig.get(key));
|
|
||||||
}
|
// Refresh @Value and @ConfigurationProperties fields
|
||||||
|
logger.info("[RefreshScope] Calling refreshDynamicConfigFields()...");
|
||||||
// Update Spring Environment property source
|
refreshDynamicConfigFields();
|
||||||
if (applicationContext != null) {
|
logger.info("[RefreshScope] refreshDynamicConfigFields() completed");
|
||||||
StandardEnvironment environment = (StandardEnvironment) applicationContext.getEnvironment();
|
}
|
||||||
MutablePropertySources propertySources = environment.getPropertySources();
|
}
|
||||||
synchronized (propertySources) {
|
|
||||||
Properties properties = new Properties();
|
private void refreshDynamicConfigFields() {
|
||||||
if (propertySources.contains(MANAGER_PROPERTY_SOURCE)) {
|
if (applicationContext != null) {
|
||||||
Object source = propertySources.get(MANAGER_PROPERTY_SOURCE);
|
try {
|
||||||
if (source instanceof MapPropertySource) {
|
logger.info("[RefreshScope] Starting dynamic config field refresh...");
|
||||||
Map<String, Object> map = ((MapPropertySource) source).getSource();
|
StandardEnvironment env = (StandardEnvironment) applicationContext.getEnvironment();
|
||||||
for (Entry<String, Object> entry : map.entrySet()) {
|
// 直接创建 RefreshScopeRegistry 并注册为 BeanPostProcessor
|
||||||
properties.put(entry.getKey(), entry.getValue());
|
RefreshScopeRegistry registry = new RefreshScopeRegistry(env);
|
||||||
}
|
|
||||||
} else if (source instanceof PropertiesPropertySource) {
|
// 遍历所有已注册的 Bean,记录 @Value 和 @ConfigurationProperties 字段
|
||||||
Map<String, Object> src = ((PropertiesPropertySource) source).getSource();
|
String[] beanNames = applicationContext.getBeanDefinitionNames();
|
||||||
for (Entry<String, Object> entry : src.entrySet()) {
|
logger.info("[RefreshScope] Found {} beans in ApplicationContext", beanNames.length);
|
||||||
properties.put(entry.getKey(), entry.getValue().toString());
|
|
||||||
}
|
int valueFieldCount = 0;
|
||||||
}
|
int configPropsCount = 0;
|
||||||
}
|
for (String beanName : beanNames) {
|
||||||
for (Entry<String, Object> entry : updateConfig.entrySet()) {
|
Object bean = applicationContext.getBean(beanName);
|
||||||
properties.put(entry.getKey(), entry.getValue());
|
registry.postProcessAfterInitialization(bean, beanName);
|
||||||
}
|
}
|
||||||
propertySources.replace(MANAGER_PROPERTY_SOURCE, new PropertiesPropertySource(MANAGER_PROPERTY_SOURCE, properties));
|
|
||||||
}
|
valueFieldCount = registry.getValueFieldCount();
|
||||||
}
|
configPropsCount = registry.getConfigPropsCount();
|
||||||
|
logger.info("[RefreshScope] Recorded {} @Value fields and {} @ConfigurationProperties beans", valueFieldCount, configPropsCount);
|
||||||
if (configChangeHanlders == null) {
|
|
||||||
configChangeHanlders = new ArrayList<>();
|
// 刷新
|
||||||
if (applicationContext != null) {
|
registry.refresh();
|
||||||
Map<String, ConfigChangeHanlder> interfaces = applicationContext.getBeansOfType(ConfigChangeHanlder.class);
|
logger.info("[RefreshScope] RefreshScopeRegistry.refresh() completed, refreshed {} @Value fields and {} @ConfigurationProperties beans", registry.getValueFieldCount(), registry.getConfigPropsCount());
|
||||||
if (interfaces != null) {
|
} catch (Exception e) {
|
||||||
configChangeHanlders.addAll(interfaces.values());
|
logger.warn("[RefreshScope] RefreshScopeRegistry.refresh() error, msg:{}", e.getMessage(), e);
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
}
|
logger.warn("[RefreshScope] ApplicationContext is null, cannot refresh dynamic config fields");
|
||||||
|
}
|
||||||
for (ConfigChangeHanlder handler : configChangeHanlders) {
|
}
|
||||||
try {
|
|
||||||
handler.onConfigChanged(updateConfig);
|
public String buildTokenParameter(String url) {
|
||||||
logger.info("invoke {}.onConfigChanged successed!", handler.getClass().getName());
|
if (token == null || token.isEmpty()) {
|
||||||
} catch (Exception e) {
|
return url;
|
||||||
logger.warn("invoke {}.onConfigChanged error, msg:{}", handler.getClass().getName(), e.getMessage());
|
}
|
||||||
}
|
return url + (url.contains("?") ? "&" : "?") + "token=" + token;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh @Value and @ConfigurationProperties fields
|
public void close() {
|
||||||
logger.info("[RefreshScope] Calling refreshDynamicConfigFields()...");
|
if (configChangeListener != null) {
|
||||||
refreshDynamicConfigFields();
|
configChangeListener.close();
|
||||||
logger.info("[RefreshScope] refreshDynamicConfigFields() completed");
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private String hideSensitive(String key, String origin) {
|
||||||
private void refreshDynamicConfigFields() {
|
if (StringUtils.isAnyBlank(key, origin)) return "";
|
||||||
if (applicationContext != null) {
|
boolean isSensitive = false;
|
||||||
try {
|
for (String k : sensitiveKeys) {
|
||||||
logger.info("[RefreshScope] Starting dynamic config field refresh...");
|
if (key.toLowerCase().contains(k)) {
|
||||||
StandardEnvironment env = (StandardEnvironment) applicationContext.getEnvironment();
|
isSensitive = true;
|
||||||
// 直接创建 RefreshScopeRegistry 并注册为 BeanPostProcessor
|
break;
|
||||||
RefreshScopeRegistry registry = new RefreshScopeRegistry(env);
|
}
|
||||||
|
}
|
||||||
// 遍历所有已注册的 Bean,记录 @Value 和 @ConfigurationProperties 字段
|
int length = origin.length();
|
||||||
String[] beanNames = applicationContext.getBeanDefinitionNames();
|
if (isSensitive && length > 1) return origin.substring(0, length / 2).concat("****");
|
||||||
logger.info("[RefreshScope] Found {} beans in ApplicationContext", beanNames.length);
|
return origin;
|
||||||
|
}
|
||||||
int valueFieldCount = 0;
|
|
||||||
int configPropsCount = 0;
|
|
||||||
for (String beanName : beanNames) {
|
|
||||||
Object bean = applicationContext.getBean(beanName);
|
|
||||||
registry.postProcessAfterInitialization(bean, beanName);
|
|
||||||
}
|
|
||||||
|
|
||||||
valueFieldCount = registry.getValueFieldCount();
|
|
||||||
configPropsCount = registry.getConfigPropsCount();
|
|
||||||
logger.info("[RefreshScope] Recorded {} @Value fields and {} @ConfigurationProperties beans", valueFieldCount, configPropsCount);
|
|
||||||
|
|
||||||
// 刷新
|
|
||||||
registry.refresh();
|
|
||||||
logger.info("[RefreshScope] RefreshScopeRegistry.refresh() completed, refreshed {} @Value fields and {} @ConfigurationProperties beans",
|
|
||||||
registry.getValueFieldCount(), registry.getConfigPropsCount());
|
|
||||||
} catch (Exception e) {
|
|
||||||
logger.warn("[RefreshScope] RefreshScopeRegistry.refresh() error, msg:{}", e.getMessage(), e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn("[RefreshScope] ApplicationContext is null, cannot refresh dynamic config fields");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean pingCcServer(String pingUrl, int retry) {
|
|
||||||
boolean result = false;
|
|
||||||
try {
|
|
||||||
result = httpGet(pingUrl) != null;
|
|
||||||
} catch (Exception e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
if (retry == 0) return false;
|
|
||||||
if (!result) {
|
|
||||||
try { Thread.sleep(1500); } catch (Exception e) { }
|
|
||||||
return pingCcServer(pingUrl, --retry);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String buildTokenParameter(String url) {
|
|
||||||
if (token == null || token.isEmpty()) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
return url + (url.contains("?") ? "&" : "?") + "token=" + token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
if (configChangeListener != null) {
|
|
||||||
configChangeListener.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object decodeEncryptIfRequire(Object data) {
|
|
||||||
if (data == null) return "";
|
|
||||||
if (data.toString().startsWith(CRYPT_PREFIX)) {
|
|
||||||
Validate.notBlank(secret, "config[kenaito.config.encrypt-secret] is required");
|
|
||||||
data = data.toString().replace(CRYPT_PREFIX, "");
|
|
||||||
String decryptString;
|
|
||||||
try {
|
|
||||||
decryptString = decryptWithAES(secret, data.toString());
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (globalSecret != null && !globalSecret.isEmpty()) {
|
|
||||||
decryptString = decryptWithAES(globalSecret, data.toString());
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return decryptString;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String hideSensitive(String key, String origin) {
|
|
||||||
if (StringUtils.isAnyBlank(key, origin)) return "";
|
|
||||||
boolean isSensitive = false;
|
|
||||||
for (String k : sensitiveKeys) {
|
|
||||||
if (key.toLowerCase().contains(k)) {
|
|
||||||
isSensitive = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
int length = origin.length();
|
|
||||||
if (isSensitive && length > 1) return origin.substring(0, length / 2).concat("****");
|
|
||||||
return origin;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
-24
@@ -1,24 +0,0 @@
|
|||||||
package cn.odboy.confcenter;
|
|
||||||
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.springframework.context.ApplicationEvent;
|
|
||||||
|
|
||||||
public class EnvironmentChangeEvent extends ApplicationEvent {
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
|
|
||||||
private Set<String> keys;
|
|
||||||
|
|
||||||
public EnvironmentChangeEvent(Set<String> keys) {
|
|
||||||
super(keys);
|
|
||||||
this.keys = keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the keys
|
|
||||||
*/
|
|
||||||
public Set<String> getKeys() {
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ type Config struct {
|
|||||||
Server ServerConfig
|
Server ServerConfig
|
||||||
Database DatabaseConfig
|
Database DatabaseConfig
|
||||||
JWT JWTConfig
|
JWT JWTConfig
|
||||||
Crypto CryptoConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -35,10 +34,6 @@ type JWTConfig struct {
|
|||||||
ExpireHours int
|
ExpireHours int
|
||||||
}
|
}
|
||||||
|
|
||||||
type CryptoConfig struct {
|
|
||||||
SecretKey string
|
|
||||||
}
|
|
||||||
|
|
||||||
var Cfg *Config
|
var Cfg *Config
|
||||||
|
|
||||||
func Load(path string) error {
|
func Load(path string) error {
|
||||||
@@ -77,8 +72,6 @@ func Load(path string) error {
|
|||||||
Cfg.JWT.Secret = baseProps["jwt.secret"]
|
Cfg.JWT.Secret = baseProps["jwt.secret"]
|
||||||
Cfg.JWT.ExpireHours = intProp(baseProps, "jwt.expireHours", 24)
|
Cfg.JWT.ExpireHours = intProp(baseProps, "jwt.expireHours", 24)
|
||||||
|
|
||||||
Cfg.Crypto.SecretKey = baseProps["crypto.secretKey"]
|
|
||||||
|
|
||||||
if Cfg.Database.Host == "" {
|
if Cfg.Database.Host == "" {
|
||||||
return fmt.Errorf("missing required config: database.host")
|
return fmt.Errorf("missing required config: database.host")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user