package com.jeesuite.confcenter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Properties; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertiesPropertySource; import org.springframework.core.env.StandardEnvironment; import com.jeesuite.common.crypt.AES; import com.jeesuite.common.crypt.Base64; import com.jeesuite.common.http.HttpRequestEntity; import com.jeesuite.common.http.HttpResponseEntity; import com.jeesuite.common.http.HttpUtils; import com.jeesuite.common.json.JsonUtils; import com.jeesuite.common.util.DigestUtils; import com.jeesuite.common.util.NodeNameHolder; import com.jeesuite.common.util.ResourceUtils; import com.jeesuite.common.util.TokenGenerator; import com.jeesuite.spring.InstanceFactory; import com.jeesuite.spring.helper.EnvironmentHelper; public class ConfigcenterContext { private final static Logger logger = LoggerFactory.getLogger("com.jeesuite"); private static ConfigcenterContext instance = new ConfigcenterContext(); public static final String MANAGER_PROPERTY_SOURCE = "configcenter"; private static final String IGNORE_PLACEHOLER = "[Ignore]"; private static final String CRYPT_PREFIX = "{Cipher}"; private Boolean remoteEnabled; private final String nodeId = NodeNameHolder.getNodeId(); private String[] apiBaseUrls; private String app; private String env; private String version; private String secret; private String globalSecret; private String tokenCryptKey; private boolean remoteFirst = false; private String zkSyncServers; private boolean isSpringboot; private int syncIntervalSeconds = 50; private InternalConfigChangeListener configChangeListener; private List<ConfigChangeHanlder> configChangeHanlders; private boolean processed; private Properties remoteProperties; private ConfigcenterContext() {} /** * @return the processed */ public boolean isProcessed() { return processed; } public synchronized void init(Properties properties,boolean isSpringboot) { if(processed || !isRemoteEnabled())return; ResourceUtils.merge(properties); System.setProperty("client.nodeId", nodeId); System.setProperty("springboot", String.valueOf(isSpringboot)); this.isSpringboot = isSpringboot; String defaultAppName = ResourceUtils.getProperty("spring.application.name"); app = ResourceUtils.getProperty("jeesuite.configcenter.appName",defaultAppName); if(remoteEnabled == null)remoteEnabled = ResourceUtils.getBoolean("jeesuite.configcenter.enabled",true); if(!isRemoteEnabled())return; env = ResourceUtils.getProperty("jeesuite.configcenter.profile","dev"); Validate.notBlank(env,"[jeesuite.configcenter.profile] is required"); setApiBaseUrl(ResourceUtils.getProperty("jeesuite.configcenter.base.url")); version = ResourceUtils.getProperty("jeesuite.configcenter.version","0.0.0"); syncIntervalSeconds = ResourceUtils.getInt("jeesuite.configcenter.sync-interval-seconds", 30); tokenCryptKey = ResourceUtils.getProperty("jeesuite.configcenter.cryptKey"); System.out.println(String.format("\n=====Configcenter config=====\nappName:%s\nenv:%s\nversion:%s\nremoteEnabled:%s\napiBaseUrls:%s\n=====Configcenter config=====", app,env,version,isRemoteEnabled(),JsonUtils.toJson(apiBaseUrls))); } public static ConfigcenterContext getInstance() { return instance; } public boolean isRemoteEnabled() { return remoteEnabled == null || remoteEnabled; } public void setRemoteEnabled(boolean remoteEnabled) { this.remoteEnabled = remoteEnabled; } public String[] getApiBaseUrls() { return apiBaseUrls; } public void setApiBaseUrl(String apiBaseUrl) { Validate.notBlank(apiBaseUrl,"[jeesuite.configcenter.base.url] is required"); String[] urls = apiBaseUrl.split(",|;"); this.apiBaseUrls = new String[urls.length]; for (int i = 0; i < urls.length; i++) { if(urls[i].endsWith("/")){ this.apiBaseUrls[i] = urls[i].substring(0, urls[i].length() - 1); }else{ this.apiBaseUrls[i] = urls[i]; } } } public String getApp() { return app; } public String getEnv() { return env; } public String getVersion() { return version; } public String getSecret() { return secret; } public boolean isRemoteFirst() { return remoteFirst; } public int getSyncIntervalSeconds() { return syncIntervalSeconds; } public String getNodeId() { return nodeId; } public boolean isSpringboot() { return isSpringboot; } public void mergeRemoteProperties(Properties properties){ if(!remoteEnabled)return; remoteProperties = getAllRemoteProperties(); if(remoteProperties != null){ //合并属性 Set<Entry<Object, Object>> entrySet = remoteProperties.entrySet(); for (Entry<Object, Object> entry : entrySet) { //本地配置优先 if(isRemoteFirst() == false && properties.containsKey(entry.getKey())){ continue; } String value = entry.getValue().toString(); if(IGNORE_PLACEHOLER.contentEquals(value))continue; properties.setProperty(entry.getKey().toString(), value); } } //替换本地变量占位符 Set<Entry<Object, Object>> entrySet = properties.entrySet(); for (Entry<Object, Object> entry : entrySet) { String key = entry.getKey().toString(); String value = entry.getValue().toString(); if(value.contains(ResourceUtils.PLACEHOLDER_PREFIX)){ value = ResourceUtils.replaceRefValue(properties, value); properties.setProperty(key, value); } ResourceUtils.add(key, value); } //register listener configChangeListener = new InternalConfigChangeListener(zkSyncServers); // printConfigs(properties); // //syncConfigToServer(properties); } private Properties getAllRemoteProperties(){ if(remoteProperties != null)return remoteProperties; Properties properties = new Properties(); Map<String,Object> map = fetchConfigFromServer(); if(map == null){ throw new RuntimeException("fetch remote config error!"); } //解密密匙 secret = Objects.toString(map.remove("jeesuite.configcenter.encrypt-secret"),null); globalSecret = Objects.toString(map.remove("jeesuite.configcenter.global-encrypt-secret"),null); remoteFirst = Boolean.parseBoolean(Objects.toString(map.remove("jeesuite.configcenter.remote-config-first"),"false")); zkSyncServers = Objects.toString(map.remove("jeesuite.configcenter.sync-zk-servers"),null); properties.putAll(map); properties.clear(); Set<String> keys = map.keySet(); for (String key : keys) { Object value = decodeEncryptIfRequire(map.get(key)); properties.put(key, value); } return properties; } @SuppressWarnings("unchecked") private Map<String,Object> fetchConfigFromServer(){ Map<String,Object> result = null; String errorMsg = null; for (String apiBaseUrl : apiBaseUrls) { String url = buildTokenParameter(String.format("%s/api/fetch_all_configs?appName=%s&env=%s&version=%s", apiBaseUrl,app,env,version)); System.out.println("fetch configs url:" + url); String jsonString = null; try { HttpResponseEntity response = HttpUtils.get(url); if(response.isSuccessed()){ jsonString = response.getBody(); result = JsonUtils.toObject(jsonString, Map.class); if(result.containsKey("code")){ errorMsg = result.get("msg").toString(); System.err.println("fetch error:"+errorMsg); result = null; }else{ break; } } } catch (Exception e) { e.printStackTrace(); } } // if(result == null){ System.out.println(">>>>>remote Config fecth error, load from local Cache"); result = LocalCacheUtils.read(); }else{ LocalCacheUtils.write(result); } return result; } private void printConfigs(Properties properties){ List<String> sortKeys = new ArrayList<>(); Set<Entry<Object, Object>> entrySet = properties.entrySet(); for (Entry<Object, Object> entry : entrySet) { String key = entry.getKey().toString(); sortKeys.add(key); } Collections.sort(sortKeys); System.out.println("==================final config list start=================="); String value; for (String key : sortKeys) { value = hideSensitive(key, properties.getProperty(key)); System.out.println(String.format("%s = %s", key,value )); } System.out.println("==================final config list end===================="); } private void syncConfigToServer(Properties properties){ if(processed)return; if(!remoteEnabled)return; Map<String, String> params = new HashMap<>(); params.put("nodeId", nodeId); params.put("appName", app); params.put("env", env); params.put("version", version); params.put("springboot", String.valueOf(isSpringboot)); params.put("syncIntervalSeconds", String.valueOf(syncIntervalSeconds)); String serverPort = ServerEnvUtils.getServerPort(); if(StringUtils.isNumeric(serverPort)){ params.put("serverport", serverPort); } //k8s POD_IP ->valueFrom:fieldRef:fieldPath:status.podIP String serverip = System.getenv("POD_IP"); if(StringUtils.isBlank(serverip)){ try {serverip = EnvironmentHelper.getProperty("spring.cloud.client.ipAddress");} catch (Exception e) {} } if(StringUtils.isNotBlank(serverip)){ params.put("serverip", serverip); }else{ params.put("serverip", ServerEnvUtils.getServerIpAddr()); } Set<Entry<Object, Object>> entrySet = properties.entrySet(); for (Entry<Object, Object> entry : entrySet) { String key = entry.getKey().toString(); String value = entry.getValue().toString(); params.put(key, hideSensitive(key, value)); } String url = buildTokenParameter(apiBaseUrls[0] + "/api/notify_final_config"); HttpResponseEntity responseEntity = HttpUtils.postJson(url, JsonUtils.toJson(params),HttpUtils.DEFAULT_CHARSET); if(responseEntity.isSuccessed()){ logger.info("syncConfigToServer[{}] Ok",url); }else{ logger.warn("syncConfigToServer[{}] error",url); } processed = true; } public synchronized void updateConfig(Map<String, Object> updateConfig){ if(!updateConfig.isEmpty()){ Set<String> keySet = updateConfig.keySet(); for (String key : keySet) { String oldValue = ResourceUtils.getProperty(key); ResourceUtils.add(key, decodeEncryptIfRequire(updateConfig.get(key)).toString()); StandardEnvironment environment = InstanceFactory.getInstance(StandardEnvironment.class); MutablePropertySources propertySources = environment.getPropertySources(); MapPropertySource source = null; synchronized (propertySources) { if(!propertySources.contains(MANAGER_PROPERTY_SOURCE)){ source = new MapPropertySource(MANAGER_PROPERTY_SOURCE, new LinkedHashMap<String, Object>()); environment.getPropertySources().addFirst(source); }else{ source = (MapPropertySource) propertySources.get(MANAGER_PROPERTY_SOURCE); } } Map<String, Object> map = (Map<String, Object>) source.getSource(); Properties properties = new Properties(); properties.putAll(map); properties.putAll(updateConfig); propertySources.replace(source.getName(), new PropertiesPropertySource(source.getName(), properties)); logger.info("Config [{}] Change,oldValue:{},newValue:{}",key,oldValue,updateConfig.get(key)); } if(configChangeHanlders == null){ configChangeHanlders = new ArrayList<>(); Map<String, ConfigChangeHanlder> interfaces = InstanceFactory.getInstanceProvider().getInterfaces(ConfigChangeHanlder.class); if(interfaces != null){ configChangeHanlders.addAll(interfaces.values()); } } for (ConfigChangeHanlder hander : configChangeHanlders) { try { hander.onConfigChanged(updateConfig); logger.info("invoke {}.onConfigChanged successed!",hander.getClass().getName()); } catch (Exception e) { e.printStackTrace(); logger.warn("invoke {}.onConfigChanged error,msg:{}",hander.getClass().getName(),e.getMessage()); } } } } public boolean pingCcServer(String pingUrl,int retry){ boolean result = false; try { System.out.println("pingCcServer ,retry:"+retry); result = HttpUtils.get(pingUrl,HttpRequestEntity.create().connectTimeout(2000).readTimeout(2000)).isSuccessed(); } catch (Exception e) {} 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(tokenCryptKey == null)return url; return url + (url.contains("?") ? "&" : "?") + "authtoken=" + TokenGenerator.generateWithSign("jeesuite.configcenter"); } public void close(){ if(configChangeListener != null){ configChangeListener.close(); } } private Object decodeEncryptIfRequire(Object data) { if (data.toString().startsWith(CRYPT_PREFIX)) { Validate.notBlank(secret,"config[jeesuite.configcenter.encrypt-secret] is required"); data = data.toString().replace(CRYPT_PREFIX, ""); String decryptString; try { decryptString = decryptWithAES(secret, data.toString()); } catch (Exception e) { decryptString = decryptWithAES(globalSecret, data.toString()); } return decryptString; } return data; } private static String decryptWithAES(String key, String data){ try { String secretKey = DigestUtils.md5(key).substring(16); byte[] bytes = AES.decrypt(Base64.decode(data.getBytes(StandardCharsets.UTF_8)), secretKey.getBytes(StandardCharsets.UTF_8)); return new String(bytes, StandardCharsets.UTF_8); } catch (Exception e) { System.err.println(String.format("解密错误:%s",data)); throw new RuntimeException(e); } } List<String> sensitiveKeys = new ArrayList<>(Arrays.asList("pass","key","secret","token","credentials")); private String hideSensitive(String key,String orign){ if(StringUtils.isAnyBlank(key,orign))return ""; boolean is = false; for (String k : sensitiveKeys) { if(is = key.toLowerCase().contains(k))break; } int length = orign.length(); if(is && length > 1)return orign.substring(0, length/2).concat("****"); return orign; } }